├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── assets ├── .DS_Store └── icons │ ├── .DS_Store │ ├── dark-mode.png │ ├── light-mode.png │ ├── mdblue-mode.png │ ├── plus-icon-rounded.svg │ ├── plus-rounded-dark.svg │ ├── reset-icon-red-rounded.svg │ ├── rule-black.svg │ ├── rule.ico │ ├── rules.svg │ └── submit-icon-green-rounded.svg ├── docs ├── README.md ├── _config.yml ├── advanced.md ├── create-rules.md ├── decisions.md ├── images │ ├── create-upload.png │ ├── create.png │ ├── decision1.png │ ├── decision2.png │ ├── decision3.png │ ├── decision4.png │ ├── decision5.png │ ├── decision6.png │ ├── fact1.png │ ├── fact2.png │ ├── fact3.png │ ├── generate.png │ ├── more-decisions1.png │ ├── more-decisions2.png │ ├── more-decisions3.png │ ├── more-decisions4.png │ ├── more-decisions5.png │ ├── more-decisions6.png │ ├── more-decisions7.png │ ├── more-decisions8.png │ ├── output-params.png │ ├── path-decisions1.png │ ├── path-decisions2.png │ ├── path-fact.png │ ├── update-decisions1.png │ ├── update-decisions2.png │ ├── update-decisions3.png │ ├── update-decisions4.png │ ├── update-decisions5.png │ ├── update-fact1.png │ ├── update-fact2.png │ ├── update-validate.png │ ├── upload.png │ └── validate.png ├── implementation.md ├── manage-rules.md └── rule-engine.md ├── examples ├── Credit-Card-Eligibility.json └── Room-Rent.json ├── index.html ├── package-lock.json ├── package.json ├── src ├── actions │ ├── action-types.js │ ├── app.js │ ├── attributes.js │ ├── decisions.js │ └── ruleset.js ├── app.js ├── components │ ├── attributes │ │ ├── add-atrribtues.js │ │ ├── attr-details.js │ │ ├── attributes.js │ │ └── view-attributes.js │ ├── button │ │ ├── button-groups.js │ │ ├── button.js │ │ └── nav-button.js │ ├── decisions │ │ ├── add-decision.js │ │ ├── decision-details.js │ │ └── decision.js │ ├── error │ │ └── ruleset-error.js │ ├── footer │ │ └── footer.js │ ├── forms │ │ ├── input-field.js │ │ └── selectmenu-field.js │ ├── loader │ │ └── loader.js │ ├── navigation │ │ ├── navigation-link.js │ │ └── navigation-panel.js │ ├── notification │ │ └── notification.js │ ├── panel │ │ ├── banner.js │ │ └── panel.js │ ├── search │ │ └── search.js │ ├── table │ │ └── table.js │ ├── tabs │ │ └── tabs.js │ ├── title │ │ ├── page-title.js │ │ └── title.js │ ├── toolbar │ │ └── toolbar.js │ ├── tree │ │ ├── tree-style.js │ │ └── tree.js │ └── validate │ │ └── validate-rules.js ├── constants │ ├── app-style.js │ ├── data-types.js │ └── messages.js ├── containers │ ├── app │ │ ├── app-container.js │ │ └── appearance-container.js │ ├── home │ │ └── home-container.js │ └── ruleset │ │ ├── create-ruleset-container.js │ │ └── ruleset-container.js ├── context │ └── apperance-context.js ├── data-objects │ ├── footer-links.json │ └── operator.json ├── reducers │ ├── app-reducer.js │ ├── index.js │ └── ruleset-reducer.js ├── routes │ └── app-routes.js ├── sass │ ├── _utils.scss │ ├── base.scss │ ├── components │ │ ├── apperance.scss │ │ ├── attributes.scss │ │ ├── button.scss │ │ ├── decisions.scss │ │ ├── forms.scss │ │ ├── home.scss │ │ ├── index.scss │ │ ├── nav.scss │ │ ├── panel.scss │ │ ├── search.scss │ │ ├── table.scss │ │ ├── tabs.scss │ │ ├── title.scss │ │ └── tree.scss │ ├── theme │ │ ├── dark.scss │ │ ├── index.scss │ │ ├── light.scss │ │ └── md-blue.scss │ └── variables.scss ├── store.js ├── utils │ ├── stringutils.js │ ├── transform.js │ └── treeutils.js └── validations │ ├── attribute-validations.js │ ├── decision-validation.js │ └── rule-validation.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":["@babel/env", "@babel/react"], 3 | "plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }], "react-hot-loader/babel", "transform-class-properties", "emotion"] 4 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/**/*.json 2 | src/**/*.scss 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react" 4 | ], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended" 8 | ], 9 | "env": { 10 | "browser": true, 11 | "es6": true, 12 | "jest": true 13 | }, 14 | "globals": { 15 | "process": true, 16 | "webpack": true, 17 | "module": true, 18 | "__dirname": true 19 | }, 20 | "parser": "@babel/eslint-parser", 21 | "settings": { 22 | "react": { 23 | "createClass": "createReactClass", 24 | "pragma": "React", 25 | "version": "detect", 26 | "flowVersion": "0.53" 27 | }, 28 | "propWrapperFunctions": [ 29 | "forbidExtraProps", 30 | {"property": "freeze", "object": "Object"}, 31 | {"property": "myFavoriteWrapper"} 32 | ] 33 | }, 34 | "rules": { 35 | "no-console": ["error", { "allow": ["warn", "error"] }] 36 | } 37 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | json-rule-editor.code-workspace 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | 66 | # General 67 | .DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Icon must end with two \r 72 | Icon 73 | 74 | 75 | # Thumbnails 76 | ._* 77 | 78 | # Files that might appear in the root of a volume 79 | .DocumentRevisions-V100 80 | .fseventsd 81 | .Spotlight-V100 82 | .TemporaryItems 83 | .Trashes 84 | .VolumeIcon.icns 85 | .com.apple.timemachine.donotpresent 86 | 87 | # Directories potentially created on remote AFP share 88 | .AppleDB 89 | .AppleDesktop 90 | Network Trash Folder 91 | Temporary Items 92 | .apdisk 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Getting Started 4 | 5 | Json rule editor is an unornamented web interface tool to manage your business rules and decision outcomes. A single page application to effortlesly upload / create business rules via json files. Since the lightweight libraries are the new normal in web applications, possessing logics in json file will be a effective way to manage the application code. Having said that redudancy can be detoured by discarding the other business rule file format such as xml or excel, which is often hectic to parse and adding complications to developer. 6 | 7 | Process to implement json rule file in your application follows 8 | 9 | - Generate rule file using json rule editor 10 | - Place the generated file in your application folder, 11 | - and pass the relative path of file and input parameters into json rules engine to get the output. 12 | 13 | Facinating feature it implies is, we can validate the business decisions instantly once after rules are added. Thus, ensuring expected outcome is returned from the rule sheet before generates the file. Consequently it eliminates issues at first place when rules are being created instead of identifing at later stage of development or testing. 14 | 15 | ### Usage: 16 | 17 | To launch the json rule editor tool, you can do either of the below 18 | 1. Click [json rule editor](https://www.json-rule-editor.com) 19 | 2. or install it locally via `git clone https://github.com/vinzdeveloper/json-rule-editor.git` 20 | - start the application by `npm install` and `npm start` 21 | 22 | The detailed steps to create json rule file using this tool in [next section](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html). 23 | 24 | ### Thanks to json-rules-engine: 25 | 26 | In principle, json rules engine is an integral backbone library should be added into your application to process the business rules created from this rule editor. It is highly recommended to go through the json rules engine concepts and explore the features it offers. 27 | 28 | Install json rules engine in your application using the steps mentioned in the [npm](https://www.npmjs.com/package/json-rules-engine) 29 | 30 | This documentation explains the steps to create / manage rules in json rule editor tool. 31 | 32 | This documentation covers, 33 | 34 | 1. [Why Rule Engine?](https://vinzdeveloper.github.io/json-rule-editor/docs/rule-engine.html) 35 | 2. [Create Business Decisions using Json Rule Editor](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) 36 | 3. [Implementation of rules in application](https://vinzdeveloper.github.io/json-rule-editor/docs/implementation.html) 37 | 4. [Manage existing rule](https://vinzdeveloper.github.io/json-rule-editor/docs/manage-rules.html) 38 | 5. [More examples in Decisions](https://vinzdeveloper.github.io/json-rule-editor/docs/decisions.html) 39 | 6. [Advanced examples](https://vinzdeveloper.github.io/json-rule-editor/docs/advanced.html) 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/.DS_Store -------------------------------------------------------------------------------- /assets/icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/icons/.DS_Store -------------------------------------------------------------------------------- /assets/icons/dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/icons/dark-mode.png -------------------------------------------------------------------------------- /assets/icons/light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/icons/light-mode.png -------------------------------------------------------------------------------- /assets/icons/mdblue-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/icons/mdblue-mode.png -------------------------------------------------------------------------------- /assets/icons/plus-icon-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard 44 4 | 5 | background 6 | 7 | 8 | 9 | Layer 1 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/icons/plus-rounded-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | Artboard 44 3 | 4 | 5 | background 6 | 7 | 8 | 9 | Layer 1 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/icons/reset-icon-red-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icons/rule-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/icons/rule.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/assets/icons/rule.ico -------------------------------------------------------------------------------- /assets/icons/rules.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/icons/submit-icon-green-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | IOS__BASIC_ICONS_FILL 3 | 4 | 5 | background 6 | 7 | 8 | 9 | Layer 1 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Getting Started 4 | 5 | Json rule editor is an unornamented web interface tool to manage your business rules and decision outcomes. A single page application to effortlesly upload / create business rules via json files. Since the lightweight libraries are the new normal in web applications, possessing logics in json file will be a effective way to manage the application code. Having said that redudancy can be detoured by discarding the other business rule file format such as xml or excel, which is often hectic to parse and adding complications to developer. 6 | 7 | Process to implement json rule file in your application follows 8 | 9 | - Generate rule file using json rule editor 10 | - Place the generated file in your application folder, 11 | - and pass the relative path of file and input parameters into json rules engine to get the output. 12 | 13 | Facinating feature it implies is, we can validate the business decisions instantly once after rules are added. Thus, ensuring expected outcome is returned from the rule sheet before generates the file. Consequently it eliminates issues at first place when rules are being created instead of identifing at later stage of development or testing. 14 | 15 | ### Usage: 16 | 17 | To launch the json rule editor tool, you can do either of the below 18 | 1. Click [json rule editor](https://www.json-rule-editor.com) 19 | 2. or install it locally via `git clone https://github.com/vinzdeveloper/json-rule-editor.git` 20 | - start the application by `npm install` and `npm start` 21 | 22 | The detailed steps to create json rule file using this tool in [next section](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html). 23 | 24 | ### Thanks to json-rules-engine: 25 | 26 | In principle, json rules engine is an integral backbone library should be added into your application to process the business rules created from this rule editor. It is highly recommended to go through the json rules engine concepts and explore the features it offers. 27 | 28 | Install json rules engine in your application using the steps mentioned in the [npm](https://www.npmjs.com/package/json-rules-engine) 29 | 30 | This documentation explains the steps to create / manage rules in json rule editor tool. 31 | 32 | This documentation covers, 33 | 34 | 1. [Why Rule Engine?](https://vinzdeveloper.github.io/json-rule-editor/docs/rule-engine.html) 35 | 2. [Create Business Decisions using Json Rule Editor](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) 36 | 3. [Implementation of rules in application](https://vinzdeveloper.github.io/json-rule-editor/docs/implementation.html) 37 | 4. [Manage existing rule](https://vinzdeveloper.github.io/json-rule-editor/docs/manage-rules.html) 38 | 5. [More examples in Decisions](https://vinzdeveloper.github.io/json-rule-editor/docs/decisions.html) 39 | 6. [Advanced examples](https://vinzdeveloper.github.io/json-rule-editor/docs/advanced.html) 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Advanced concepts of json-rules-engine 4 | 5 | json rules engine provides other feature to load the input fact value aynchronously from remote system via api. 6 | Go through the advance concepts explained in detail [here](https://github.com/CacheControl/json-rules-engine) to understand better. 7 | 8 | We will take the same example *"Employee-Salary"* to handle path parameter. Lets assume, `designation`, `experience` and `type` fact values are coming via api. 9 | 10 | Step by Step process explained below to add path parameter along with facts in decisions 11 | 12 | ***Step1: Add object reference*** 13 | 14 | 1. Go to Facts tab 15 | 2. Click **Add icon** at right corner of tool bar. 16 | 3. Specify the fact object name and select **object** as type 17 | 4. Click **Add facts** button 18 | 19 | ![create path fact](https://vinzdeveloper.github.io/json-rule-editor/docs/images/path-fact.png) 20 | 21 | ***Step2: Add path parameter reference*** 22 | 23 | 1. Go to Decisions tab 24 | 2. Click **Add icon** at right corner of tool bar. 25 | 3. Select all / any in **"Step 1: Add Toplevel"** 26 | 4. Select Add facts menu in **"Step 2: Add / Remove facts"** 27 | 5. Select **Add path** icon at top right corner of panel. 28 | 6. Give fact object name, operator, value along with path parameter as below 29 | 30 | ![create path decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/path-decisions1.png) 31 | 32 | 7. Add other facts with path parameter 33 | 34 | ![create path decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/path-decisions2.png) 35 | 36 | 8. Click **Add Rulecase** button 37 | 38 | ***Caution*** 39 | 40 | > You can’t validate such path parameter decisions in our rule editor though, because these values are coming from real time api, 41 | > so its not possible to retrieve and validate such decisions in this tool. 42 | 43 | ### Add more parameters in output 44 | 45 | Sometimes, it would make sense to add more values along with output value. In order to add extra values, you can click ***‘Add Params’*** icon in output panel. You can add any number of key value pairs along with the output type. 46 | 47 | ![create path decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/output-params.png) 48 | 49 | 50 | ### Examples 51 | 52 | Couple of sample examples are available [here](https://github.com/vinzdeveloper/json-rule-editor) under **examples** folder. Play with these files by importing it into rule editor. 53 | 54 | Feel free to raise issues or concerns in [github](https://github.com/vinzdeveloper/json-rule-editor/issues) or contact here . Happy to look into your concerns!! 55 | 56 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/decisions.html) 57 | 58 | [Go Home](https://vinzdeveloper.github.io/json-rule-editor/docs) -------------------------------------------------------------------------------- /docs/create-rules.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Create Decisions 4 | 5 | Primarily, we are going to deal with two factors 1) Facts and 2) Decisions across this tool. 6 | 7 | ***Facts*** 8 | 9 | Its an entity or attribute helps to build your business decisions. It supports four data types such as number, string, array and object - inline with json rules engine. 10 | 11 | ***Decisions*** 12 | 13 | It is actually business rule conditions which contains the possible facts values and associated outcome type. You can define any number of business decision outcomes under single rule file. 14 | 15 | *Note:* 16 | 17 | > Its good practise to separate your business rules into different files based on the use cases. 18 | 19 | Lets see the step by step process starting from creating facts to generating the ruleset file. 20 | 21 | ***Step1: Create new ruleset file*** 22 | 23 | 1. Launch [json rule editor](https://www.json-rule-editor.com) or install locally via git clone 24 | 2. Click **Create button** (Note: Upload functioanlity is explained in next section) 25 | 26 | ![create new rule](https://vinzdeveloper.github.io/json-rule-editor/docs/images/create-upload.png) 27 | 28 | ***Step2: Specify rule name*** 29 | 30 | 1. Specify rule name and click **Create button** 31 | 32 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/create.png) 33 | 34 | ***Step3: Add new fact*** 35 | 36 | 1. Click **Create Facts** button at information message panel 37 | 38 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/fact1.png) 39 | 40 | 2. Give fact name and data type such as string, number or array 41 | 42 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/fact2.png) 43 | 44 | 3. Add as many as fact you need to build your decisions 45 | 46 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/fact3.png) 47 | 48 | 49 | ***Step4: Add new decisions*** 50 | 51 | Go to Decisions tab 52 | 53 | 1. Click **Create Decision** button at information message panel 54 | 55 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision1.png) 56 | 57 | 2. Select all / any in **"Step 1: Add Toplevel"** 58 | 59 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision2.png) 60 | 61 | 3. Select Add facts menu in **"Step 2: Add / Remove facts"** 62 | 63 | 4. Select the fact, operator and value 64 | 65 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision3.png) 66 | 67 | 5. Add necessary facts and you will see something as below 68 | 69 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision4.png) 70 | 71 | 6. Select the Add Outcome in **"Step 3: Add outcome""** and specify the type value. 72 | 73 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision5.png) 74 | 75 | 7. Click **Add Rulecase** button. 76 | 77 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/decision6.png) 78 | 79 | ***Step5: Validate decisions*** 80 | 81 | Go to Validate tab 82 | 83 | 1. Specify the values against facts 84 | 2. Click Validate Ruleset button 85 | 86 | 87 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/validate.png) 88 | 89 | *Note: Please keep in mind when adding facts and value* 90 | > Incase expected values are not getting displayed, go to Decisions tab and cross check the values. 91 | > Values are case sensitive and perform strict equality comparison ( === ). 92 | 93 | 94 | ***Step5: Generate rule sheet*** 95 | 96 | Go to Generate tab 97 | 98 | 1. Click generate button if all required decisions are added. 99 | 100 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/generate.png) 101 | 102 | *Note:* 103 | 104 | > Json rule file will be generated at your default browser download folder. 105 | 106 | 107 | 108 | 109 | [Next section - Implementation of Json rules in application](https://vinzdeveloper.github.io/json-rule-editor/docs/implementation.html) 110 | 111 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/rule-engine.html) 112 | 113 | [Go Home](https://vinzdeveloper.github.io/json-rule-editor/docs/) 114 | -------------------------------------------------------------------------------- /docs/decisions.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### More examples in Decisions 4 | 5 | We have seen simple rule structure example before which has just one top node *"all"*. However, Json rules engine offers many features which will be useful to manage business decisions effectively. 6 | 7 | ***Nested ‘all’ or ‘any’*** 8 | 9 | You can configure the decisions with nested all or any combination as mentioned below 10 | 11 | We will use the same rule file Employee-Salary created before for this section. 12 | 13 | Previously, we have configured facts such as designation, type and experience to derive *salary* for couple of designations **Manager** and **Contractor** to get better understandings. However, in real time, we might have other designation roles and facts to consider. Lets see a example below to understand much better 14 | 15 | For ex: 16 | 17 | We get new requirement to add *"Architect"* role with the same salary as *"Manager"* role. To handle this scenario, we can do either of the below 18 | 19 | 1. Create a new decision with same salary. 20 | 21 | ![create new decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions1.png) 22 | 23 | 2. Or You can modify the existing decision by nesting with **any** condition as below 24 | 25 | ![create another decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions2.png) 26 | 27 | 28 | First solution is same as we discussed before. Please refer [previous section](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) incase any queries, 29 | 30 | Lets explore the second solution to understand how nested conditions can be created. 31 | 32 | *Step 1: Add new decision with two facts experience and type* 33 | 34 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions3.png) 35 | 36 | *Step 2: Select "Add Any" menu in **"Step 2: Add / Remove facts"** - you will get something as below* 37 | 38 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions4.png) 39 | 40 | *Step 3: Click "any" node as highlighted below to add facts under this node* 41 | 42 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions5.png) 43 | 44 | *Step 4: Select "Add Facts" menu in **"Step 2: Add / Remove facts"** and click Add button* 45 | 46 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions6.png) 47 | 48 | **After added:** 49 | 50 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions7.png) 51 | 52 | *Step 5: Select "Add Facts" menu in **"Step 2: Add / Remove facts"** and click Add button to add another role "Architect"* 53 | 54 | ![create nested decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/more-decisions8.png) 55 | 56 | *Step 6: Select "Add outcome" menu in **"Step 3: Add outcome"** and give value* 57 | 58 | *Step 7: Click Add rulecase button* 59 | 60 | That's it!!! 61 | 62 | Likewise, you can add any number of nested nodes under single conditions. Both solution 1 and solution 2 are doing the same thing, depends on business requirement and number of facts, decide the pattern whichver it suits better. 63 | 64 | [Next section - Advanced examples](https://vinzdeveloper.github.io/json-rule-editor/docs/advanced.html) 65 | 66 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/manage-rules.html) 67 | 68 | [Go Home](https://vinzdeveloper.github.io/json-rule-editor/docs/) 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/images/create-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/create-upload.png -------------------------------------------------------------------------------- /docs/images/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/create.png -------------------------------------------------------------------------------- /docs/images/decision1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision1.png -------------------------------------------------------------------------------- /docs/images/decision2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision2.png -------------------------------------------------------------------------------- /docs/images/decision3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision3.png -------------------------------------------------------------------------------- /docs/images/decision4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision4.png -------------------------------------------------------------------------------- /docs/images/decision5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision5.png -------------------------------------------------------------------------------- /docs/images/decision6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/decision6.png -------------------------------------------------------------------------------- /docs/images/fact1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/fact1.png -------------------------------------------------------------------------------- /docs/images/fact2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/fact2.png -------------------------------------------------------------------------------- /docs/images/fact3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/fact3.png -------------------------------------------------------------------------------- /docs/images/generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/generate.png -------------------------------------------------------------------------------- /docs/images/more-decisions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions1.png -------------------------------------------------------------------------------- /docs/images/more-decisions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions2.png -------------------------------------------------------------------------------- /docs/images/more-decisions3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions3.png -------------------------------------------------------------------------------- /docs/images/more-decisions4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions4.png -------------------------------------------------------------------------------- /docs/images/more-decisions5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions5.png -------------------------------------------------------------------------------- /docs/images/more-decisions6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions6.png -------------------------------------------------------------------------------- /docs/images/more-decisions7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions7.png -------------------------------------------------------------------------------- /docs/images/more-decisions8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/more-decisions8.png -------------------------------------------------------------------------------- /docs/images/output-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/output-params.png -------------------------------------------------------------------------------- /docs/images/path-decisions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/path-decisions1.png -------------------------------------------------------------------------------- /docs/images/path-decisions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/path-decisions2.png -------------------------------------------------------------------------------- /docs/images/path-fact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/path-fact.png -------------------------------------------------------------------------------- /docs/images/update-decisions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-decisions1.png -------------------------------------------------------------------------------- /docs/images/update-decisions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-decisions2.png -------------------------------------------------------------------------------- /docs/images/update-decisions3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-decisions3.png -------------------------------------------------------------------------------- /docs/images/update-decisions4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-decisions4.png -------------------------------------------------------------------------------- /docs/images/update-decisions5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-decisions5.png -------------------------------------------------------------------------------- /docs/images/update-fact1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-fact1.png -------------------------------------------------------------------------------- /docs/images/update-fact2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-fact2.png -------------------------------------------------------------------------------- /docs/images/update-validate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/update-validate.png -------------------------------------------------------------------------------- /docs/images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/upload.png -------------------------------------------------------------------------------- /docs/images/validate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinzdeveloper/json-rule-editor/8543cd065aff8a284d281433f44ae1d846a789dd/docs/images/validate.png -------------------------------------------------------------------------------- /docs/implementation.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Implementation of Json rules in application 4 | 5 | ***Step1: Install json-rules-engine*** 6 | 7 | 1. Install json-rules-engine from [npm](https://www.npmjs.com/package/json-rules-engine) 8 | 9 | `npm install json-rules-engine` 10 | 11 | ***Step2: Place the json rule file*** 12 | 13 | 1. Place the generated rule file (as generated in [previous section](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html)) in your `src/` directory 14 | 15 | for ex: `src/rules/Employee-Salary.json` 16 | 17 | ***Step3: Pass json rule and input into json-rules-engine library*** 18 | 19 | 1. Import json rules engine 20 | 2. Import json rule file 21 | 3. Pass the **decisions** property of json rule and input parameters into Json rule engine as mentioned below 22 | 23 | for ex: 24 | 25 | lets create the file called rule-validation.js under src/validation/ 26 | 27 | import { Engine } from 'json-rules-engine'; 28 | 29 | // employeesalary is the json rule generated from rule editor 30 | 31 | import employeesalary from '../rules/Employee-Salary.json'; 32 | 33 | const processEngine = (inputs, decisions) => { 34 | 35 | // Pass the decisions into Engine constructor 36 | const engine = new Engine(decisions); 37 | 38 | // Pass the inputs here 39 | return engine.run(inputs) 40 | .then(results => { 41 | console.log(results.events); 42 | return results.events 43 | }) 44 | }; 45 | 46 | // Creating input parameter 47 | const inputs = { designation: 'Manager', experience: 14 }; 48 | 49 | 50 | // Pass the decisions property from employeesalary rule object 51 | processEngine(inputs, employeesalary.decisions); 52 | 53 | 54 | *Note:* 55 | > Please refer the [json rules engine](https://github.com/CacheControl/json-rules-engine) site 56 | > to understand the features and advanced concepts. 57 | 58 | 59 | [Next section - Manage existing rule](https://vinzdeveloper.github.io/json-rule-editor/docs/manage-rules.html) 60 | 61 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) 62 | 63 | [Go Home](https://vinzdeveloper.github.io/json-rule-editor/docs/) -------------------------------------------------------------------------------- /docs/manage-rules.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Updating existing rules: 4 | 5 | To edit / view / validate the existing rule files, upload the ruleset directory, where you maintain the rule files like application src directory or anything else, or drag and drop the appropriate files into rule editor. 6 | 7 | *Note:* 8 | 9 | > There is no restrictions in place to limit the number of files to be imported. 10 | > However, it wouldn’t allow you to import / create the duplicate rule name to avoid confusion. (File names are case sensitive) 11 | 12 | We will take the same example *"Employee Salary"* for this section, lets assume there is a ask from business to add another fact called *"employee type"* (contractor or permanent) to decide the salary criteria. We will see below how to update the existing rules in such situation. 13 | 14 | ***Step1: Upload existing rules*** 15 | 16 | 1. Launch [json rule editor](https://www.json-rule-editor.com) or install locally via git clone 17 | 2. Select the *‘Choose ruleset directory’* to select directory or drag and drop the rule files. 18 | 3. Click **Upload** button 19 | 20 | ![upload](https://vinzdeveloper.github.io/json-rule-editor/docs/images/upload.png) 21 | 22 | ***Step2: Add new fact*** 23 | 24 | Go to Employee-Salary rule. If you have just only one rule file, its selected by default 25 | 26 | 1. Click **Add icon** at top right corner of tool bar. 27 | 2. Give new fact name and data type. 28 | 29 | ![add new fact](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-fact1.png) 30 | 31 | 3. Click **Add Facts** button 32 | 33 | ![add new fact](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-fact2.png) 34 | 35 | 36 | ***Step3: Update the existing decisions with new fact*** 37 | 38 | Go to Decisions tab 39 | 40 | 1. Click **View Conditions** of outcome you want to modify. 41 | 2. Click **edit icon** at top right corner of decision panel displayed. 42 | 43 | ![update decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-decisions1.png) 44 | 45 | 3. Select the top node in decision panel where you want to add additional fact. In this case, select **all** top node as highlighted in red circle 46 | 47 | ![update decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-decisions2.png) 48 | 49 | 4. Select Add facts menu in **"Step 2: Add / Remove facts"** and fill in the new fact value 50 | 51 | ![update decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-decisions3.png) 52 | 53 | 5. Click **Edit Rulecase** button 54 | 55 | 56 | *Note:* 57 | 58 | > If you want to edit existing fact value in decision, like, changing the experience fact from 10 to 11 or 12 years. you have to delete the particular fact by 59 | > selecting the *node* and Click **Remove** in **"Step 2: Add / Remove facts"** 60 | 61 | 62 | ***Step4: Add new decision with new fact*** 63 | 64 | We will add one more new decision to handle another employment type say **Contractor** with different salary outcome. 65 | 66 | 1. Click **Add icon** at right corner of tool bar. 67 | 2. Repeat the steps of creating new decisions as mentioned in the [Step 4: here](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) 68 | 3. Add all three facts like designation, experience and type 69 | 70 | ![update decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-decisions4.png) 71 | 72 | 4. Specify the outcome value to this new decisions and Click 'Add Rulecase' button 73 | 74 | ![update decision](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-decisions5.png) 75 | 76 | ***Step5: Validate decisions*** 77 | 78 | Go to Validate tab 79 | 80 | 1. Specify the values against facts 81 | 2. Click validate button to verify new values are returning for employee type *'Contractor'* 82 | 83 | ![rule name](https://vinzdeveloper.github.io/json-rule-editor/docs/images/update-validate.png) 84 | 85 | ***Step5: Generate rule sheet*** 86 | 87 | Go to Generate tab 88 | 89 | 1. Click generate button if you are happy to proceed. 90 | 91 | 92 | *Note:* 93 | 94 | > Json rule file will be generated at your default browser download folder. 95 | 96 | 97 | **Caution:** 98 | > Please be careful when you remove / edit fact name under Facts tab in the existing rule file. 99 | > The updated changes of facts wouldn't be reflected into the decision conditions. 100 | > Also, it might impact the validation functionality as well as existing decisison functionalities. 101 | 102 | [Next section - More examples in Decisions](https://vinzdeveloper.github.io/json-rule-editor/docs/decisions.html) 103 | 104 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/implementation.html) 105 | 106 | [Go Home](https://vinzdeveloper.github.io/json-rule-editor/docs/) -------------------------------------------------------------------------------- /docs/rule-engine.md: -------------------------------------------------------------------------------- 1 | ## json-rule-editor 2 | 3 | ### Why Rule Engine? 4 | 5 | It’s obvious that coupling your business rules along with your application code is cumbersome, tedious to maintain and often end up with multiple nested if and switch case conditions. Separating the business logic from your application code would give many benefits such as 6 | 7 | - Clean code and Readable 8 | - Pluggable rules and can use it across application; reusability 9 | - And, Easy to maintain 10 | 11 | > Please go through the concepts of rule engine and understand when and which cases it actually required in your application if you are not familiar with. 12 | 13 | ### json-rules-engine 14 | 15 | [json rules engine](https://github.com/CacheControl/json-rules-engine) is the effective rule engine can be easily hooked into any server based or web based application just like other javascript libraries, besides you can maintain the application business rules in json files. 16 | 17 | ### What is Json Rule Engine and Json rule editor? 18 | 19 | It is really important to understand the basic and fundamental differences between json rule editor and json rules engine. 20 | 21 | - **json rule editor** is a web based tool to create / manage / view the business rules. Primarily to generate business rules into json file. It **doesn’t need** to be added into your application code. 22 | 23 | - And, **json rules engine** should be imported into your application and eventually you should pass the json file (generated from json rule editor app) along with input parameters to determine the outcomes. 24 | 25 | It is fairly straight forward process to incorporate the json rules in your application. Its completely fine if you are bit struggling to catch up how it’s actually works in real world. It would make more sense if you go through the further sections - explained in detail. 26 | 27 | 28 | Lets see how to create json rule file using this tool in next section 29 | 30 | [Next section - Create decisions using rule editor](https://vinzdeveloper.github.io/json-rule-editor/docs/create-rules.html) 31 | 32 | [Go back to previous page](https://vinzdeveloper.github.io/json-rule-editor/docs/) 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/Credit-Card-Eligibility.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Credit-Card-Eligibility", 3 | "attributes": [{ "name": "nationality", "type": "string" }, 4 | { "name": "age", "type": "number"}, 5 | { "name": "creditScore", "type": "number" }, 6 | { "name": "income", "type": "number"}, 7 | { "name": "job", "type": "string" }], 8 | 9 | "decisions": [{ 10 | "conditions": { 11 | "all": [{ 12 | "fact": "nationality", 13 | "operator": "equal", 14 | "value": "France" 15 | }, 16 | { 17 | "fact": "income", 18 | "operator": "greaterThan", 19 | "value": 1000 20 | }, 21 | { 22 | "fact": "job", 23 | "operator": "equal", 24 | "value": "Yes" 25 | }, 26 | { 27 | "fact": "creditScore", 28 | "operator": "greaterThan", 29 | "value": 200 30 | } 31 | ]}, 32 | "event": { 33 | "type": "Eligible" 34 | } 35 | }, 36 | { 37 | "conditions": { 38 | "all": [{ 39 | "fact": "nationality", 40 | "operator": "notEqual", 41 | "value": "France" 42 | }, 43 | 44 | { 45 | "fact": "job", 46 | "operator": "equal", 47 | "value": "Yes" 48 | }, 49 | { 50 | "any": [{ 51 | "fact": "income", 52 | "operator": "greaterThan", 53 | "value": 2000 54 | }, 55 | { 56 | "fact": "creditScore", 57 | "operator": "greaterThan", 58 | "value": 400 59 | }] 60 | } 61 | ]}, 62 | "event": { 63 | "type": "Eligible" 64 | } 65 | }, 66 | { 67 | "conditions": { 68 | "all": [{ 69 | "fact": "nationality", 70 | "operator": "notEqual", 71 | "value": "France" 72 | }, 73 | 74 | { 75 | "fact": "job", 76 | "operator": "equal", 77 | "value": "Yes" 78 | }, 79 | { 80 | "fact": "age", 81 | "operator": "lessThan", 82 | "value": 18 83 | }, 84 | { 85 | "any": [{ 86 | "fact": "income", 87 | "operator": "greaterThan", 88 | "value": 2000 89 | }, 90 | { 91 | "fact": "creditScore", 92 | "operator": "greaterThan", 93 | "value": 400 94 | }] 95 | } 96 | ]}, 97 | "event": { 98 | "type": "Eligible" 99 | } 100 | }, 101 | 102 | { 103 | "conditions": { 104 | "all": [{ 105 | "fact": "nationality", 106 | "operator": "notEqual", 107 | "value": "France" 108 | }, 109 | 110 | { 111 | "fact": "job", 112 | "operator": "notEqual", 113 | "value": "Yes" 114 | } 115 | ]}, 116 | "event": { 117 | "type": "Not Eligible" 118 | } 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /examples/Room-Rent.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Room-Rent", 3 | "attributes": [{ "name": "rooms", "type": "number" }, 4 | { "name": "bathroom", "type": "number"}, 5 | { "name": "floor", "type": "number" }, 6 | { "name": "metropolitan", "type": "string"}, 7 | { "name": "carparking", "type": "string" }], 8 | 9 | "decisions": [{ 10 | "conditions": { 11 | "all": [{ 12 | "fact": "rooms", 13 | "operator": "equal", 14 | "value": 1 15 | }, 16 | { 17 | "fact": "bathroom", 18 | "operator": "equal", 19 | "value": 1 20 | }, 21 | { 22 | "fact": "metropolitan", 23 | "operator": "equal", 24 | "value": "No" 25 | } 26 | ]}, 27 | "event": { 28 | "type": "£500" 29 | } 30 | }, 31 | { 32 | "conditions": { 33 | "all": [{ 34 | "fact": "rooms", 35 | "operator": "equal", 36 | "value": 1 37 | }, 38 | { 39 | "fact": "bathroom", 40 | "operator": "equal", 41 | "value": 1 42 | }, 43 | { 44 | "fact": "metropolitan", 45 | "operator": "equal", 46 | "value": "Yes" 47 | } 48 | ]}, 49 | "event": { 50 | "type": "£750" 51 | } 52 | }, 53 | { 54 | "conditions": { 55 | "all": [{ 56 | "fact": "rooms", 57 | "operator": "equal", 58 | "value": 2 59 | }, 60 | { 61 | "fact": "bathroom", 62 | "operator": "equal", 63 | "value": 1 64 | }, 65 | { 66 | "fact": "metropolitan", 67 | "operator": "equal", 68 | "value": "No" 69 | } 70 | ]}, 71 | "event": { 72 | "type": "£750" 73 | } 74 | }, 75 | { 76 | "conditions": { 77 | "any": [{ 78 | "all": [{ 79 | "fact": "rooms", 80 | "operator": "equal", 81 | "value": 2 82 | }, 83 | { 84 | "fact": "bathroom", 85 | "operator": "equal", 86 | "value": 1 87 | }, 88 | { 89 | "fact": "metropolitan", 90 | "operator": "equal", 91 | "value": "Yes" 92 | } 93 | ]}, 94 | { 95 | "all": [{ 96 | "fact": "rooms", 97 | "operator": "equal", 98 | "value": 3 99 | }, 100 | { 101 | "fact": "bathroom", 102 | "operator": "equal", 103 | "value": 2 104 | }, 105 | { 106 | "fact": "metropolitan", 107 | "operator": "equal", 108 | "value": "No" 109 | } 110 | ]} 111 | ]}, 112 | "event": { 113 | "type": "£1000" 114 | } 115 | } , 116 | { 117 | "conditions": { 118 | "any": [{ 119 | "all": [{ 120 | "fact": "rooms", 121 | "operator": "equal", 122 | "value": 3 123 | }, 124 | { 125 | "fact": "bathroom", 126 | "operator": "equal", 127 | "value": 2 128 | }, 129 | { 130 | "fact": "carparking", 131 | "operator": "equal", 132 | "value": "Yes" 133 | }, 134 | { 135 | "fact": "metropolitan", 136 | "operator": "equal", 137 | "value": "No" 138 | } 139 | ]}, 140 | { 141 | "all": [{ 142 | "fact": "rooms", 143 | "operator": "equal", 144 | "value": 3 145 | }, 146 | { 147 | "fact": "bathroom", 148 | "operator": "equal", 149 | "value": 2 150 | }, 151 | { 152 | "fact": "carparking", 153 | "operator": "equal", 154 | "value": "No" 155 | }, 156 | { 157 | "fact": "metropolitan", 158 | "operator": "equal", 159 | "value": "Yes" 160 | } 161 | ]} 162 | ]}, 163 | "event": { 164 | "type": "£1250" 165 | } 166 | } 167 | 168 | ] 169 | 170 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rule Editor 8 | 9 | 10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-rule-editor", 3 | "version": "1.0.0", 4 | "description": "json-rule-editor", 5 | "homepage": "https://github.com/vinzdeveloper/json-rule-editor", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --hot", 9 | "lint": "eslint 'src/**/*.js' -f table --max-warnings 0", 10 | "build": "webpack --mode=production" 11 | }, 12 | "author": "vinzdeveloper@gmail.com", 13 | "license": "ISC", 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "npm run lint" 17 | } 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/vinzdeveloper/json-rule-editor.git" 22 | }, 23 | "engines": { 24 | "node": ">=10.15.3", 25 | "npm": ">=6.4.1" 26 | }, 27 | "dependencies": { 28 | "@babel/core": "7.20.2", 29 | "@babel/plugin-transform-runtime": "7.19.6", 30 | "@babel/preset-env": "7.20.2", 31 | "@babel/preset-react": "7.18.6", 32 | "@babel/runtime": "7.20.1", 33 | "@fortawesome/fontawesome-svg-core": "6.2.1", 34 | "@fortawesome/free-solid-svg-icons": "6.2.1", 35 | "@fortawesome/react-fontawesome": "0.2.0", 36 | "babel": "6.23.0", 37 | "babel-loader": "9.1.0", 38 | "babel-plugin-transform-class-properties": "6.24.1", 39 | "css-loader": "3.4.2", 40 | "file-loader": "6.2.0", 41 | "font-awesome": "4.7.0", 42 | "history": "4.10.1", 43 | "html-webpack-plugin": "5.5.0", 44 | "json-rules-engine": "6.1.2", 45 | "lodash": "4.17.21", 46 | "mini-css-extract-plugin": "2.6.1", 47 | "node-sass": "8.0.0", 48 | "postcss-loader": "3.0.0", 49 | "react": "16.12.0", 50 | "react-bootstrap-sweetalert": "5.1.9", 51 | "react-d3-tree": "1.16.1", 52 | "react-dom": "16.12.0", 53 | "react-notifications": "1.6.0", 54 | "react-redux": "7.1.3", 55 | "react-router-dom": "5.1.2", 56 | "react-spinners": "0.8.1", 57 | "redux": "4.0.5", 58 | "redux-thunk": "2.3.0", 59 | "sass-loader": "13.2.0", 60 | "selectn": "1.1.2", 61 | "style-loader": "1.1.2", 62 | "url-loader": "4.1.0", 63 | "webpack": "5.75.0", 64 | "webpack-cli": "4.10.0" 65 | }, 66 | "devDependencies": { 67 | "@hot-loader/react-dom": "16.13.0", 68 | "@babel/eslint-parser": "7.19.1", 69 | "eslint": "8.27.0", 70 | "eslint-plugin-react": "7.31.10", 71 | "husky": "8.0.2", 72 | "react-hot-loader": "4.12.20", 73 | "redux-devtools-extension": "2.13.8", 74 | "webpack-dev-server": "4.11.1" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/actions/action-types.js: -------------------------------------------------------------------------------- 1 | export const ADD_DECISION = 'ADD_DECISION'; 2 | export const REMOVE_DECISION = 'REMOVE_DECISION'; 3 | export const REMOVE_DECISIONS = 'REMOVE_DECISIONS'; 4 | export const UPDATE_DECISION = 'UPDATE_DECISION'; 5 | export const RESET_DECISION = 'RESET_DECISION'; 6 | export const ADD_ATTRIBUTE = 'ADD_ATTRIBUTE'; 7 | export const RESET_ATTRIBUTE = 'RESET_ATTRIBUTE'; 8 | export const REMOVE_ATTRIBUTE = 'REMOVE_ATTRIBUTE'; 9 | export const UPDATE_ATTRIBUTE = 'UPDATE_ATTRIBUTE'; 10 | export const UPLOAD_RULESET = 'UPLOAD_RULESET'; 11 | export const ADD_RULESET = 'ADD_RULESET'; 12 | export const UPDATE_RULESET_INDEX = 'UPDATE_RULESET_INDEX'; 13 | export const UPDATE_NAV_STATE = 'UPDATE_NAV_STATE'; 14 | export const LOG_IN = 'LOG_IN'; 15 | -------------------------------------------------------------------------------- /src/actions/app.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_NAV_STATE, LOG_IN } from './action-types'; 2 | 3 | 4 | export function updateState(flag) { 5 | return ({ 6 | type: UPDATE_NAV_STATE, 7 | payload: { flag } 8 | }); 9 | } 10 | 11 | export function login(action) { 12 | return ({ 13 | type: LOG_IN, 14 | }); 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/actions/attributes.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './action-types'; 2 | 3 | export const add = (attribute) => { 4 | const payload = { attribute }; 5 | return ({ type: ActionTypes.ADD_ATTRIBUTE, payload}); 6 | } 7 | 8 | export const update = (attribute, index) => { 9 | const payload = { attribute, index }; 10 | 11 | return ({ type: ActionTypes.UPDATE_ATTRIBUTE, payload}); 12 | } 13 | 14 | export const remove = (attribute, index) => { 15 | const payload = { attribute, index }; 16 | 17 | return ({ type: ActionTypes.REMOVE_ATTRIBUTE, payload}); 18 | } 19 | 20 | export const reset = () => { 21 | return ({type: ActionTypes.RESET_ATTRIBUTE}) 22 | } 23 | 24 | 25 | export const handleAttribute = (action, attribute, index) => (dispatch) => { 26 | switch(action) { 27 | case 'ADD': 28 | return dispatch(add(attribute)); 29 | case 'UPDATE': 30 | return dispatch(update(attribute, index)); 31 | case 'REMOVE': 32 | return dispatch(remove(attribute, index)); 33 | case 'RESET': 34 | return dispatch(reset()); 35 | } 36 | }; -------------------------------------------------------------------------------- /src/actions/decisions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './action-types'; 2 | 3 | export const removeDecision = (decisionIndex) => { 4 | const payload = { decisionIndex }; 5 | 6 | return ({ type: ActionTypes.REMOVE_DECISION, payload}); 7 | } 8 | 9 | export const updateDecision = (condition, decisionIndex) => { 10 | const payload = { condition, decisionIndex }; 11 | 12 | return ({ type: ActionTypes.UPDATE_DECISION, payload}); 13 | } 14 | 15 | export const addDecision = (condition) => { 16 | const payload = { condition }; 17 | return ({ type: ActionTypes.ADD_DECISION, payload}); 18 | } 19 | 20 | export const removeDecisions = (outcome) => { 21 | const payload = { outcome }; 22 | 23 | return ({ type: ActionTypes.REMOVE_DECISIONS, payload}); 24 | } 25 | 26 | export const reset = () => { 27 | return ({type: ActionTypes.RESET_DECISION}); 28 | } 29 | 30 | export const handleDecision = (action, editDecision={}) => (dispatch) => { 31 | const { condition } = editDecision; 32 | switch(action) { 33 | case 'ADD': { 34 | return dispatch(addDecision(condition)); 35 | } 36 | case 'UPDATE': { 37 | const { decisionIndex } = editDecision; 38 | return dispatch(updateDecision(condition, decisionIndex)); 39 | } 40 | case 'REMOVECONDITION': { 41 | const { decisionIndex } = editDecision; 42 | return dispatch(removeDecision(decisionIndex)); 43 | } 44 | case 'REMOVEDECISIONS': { 45 | const { outcome } = editDecision; 46 | return dispatch(removeDecisions(outcome)); 47 | } 48 | case 'RESET': { 49 | return dispatch(reset()); 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/actions/ruleset.js: -------------------------------------------------------------------------------- 1 | 2 | import * as ActionTypes from './action-types'; 3 | import { updateState } from './app'; 4 | 5 | 6 | export const uploadRuleset = (ruleset) => (dispatch) => { 7 | dispatch(updateState('open')); 8 | return dispatch({ 9 | type: ActionTypes.UPLOAD_RULESET, 10 | payload: { ruleset } 11 | }); 12 | } 13 | 14 | export const addRuleset = (name) => (dispatch) => { 15 | dispatch(updateState('open')); 16 | return dispatch({ 17 | type: ActionTypes.ADD_RULESET, 18 | payload: { name } 19 | }); 20 | } 21 | 22 | export const updateRulesetIndex = (name) => { 23 | return ({ 24 | type: ActionTypes.UPDATE_RULESET_INDEX, 25 | payload: { name } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 2 | import { AppContainer } from 'react-hot-loader'; 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import configureStore from './store'; 6 | import { Provider } from 'react-redux'; 7 | import ApplicationContainer from './containers/app/app-container'; 8 | 9 | 10 | const store = configureStore(); 11 | 12 | const component = (Root) => 13 | render( 14 | 15 | 16 | 17 | , document.getElementById('root')); 18 | 19 | 20 | component(ApplicationContainer); 21 | 22 | if (module.hot) { 23 | module.hot.accept('./containers/app/app-container.js', () => { 24 | component(ApplicationContainer); 25 | }); 26 | } 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/attributes/add-atrribtues.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Panel from '../panel/panel'; 4 | import InputField from '../forms/input-field'; 5 | import SelectField from '../forms/selectmenu-field'; 6 | import Button from '../button/button'; 7 | import attributeValidations from '../../validations/attribute-validations'; 8 | import dataTypes from '../../data-objects/operator.json'; 9 | 10 | class AddAttributes extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = {error: {}, name: props.attribute.name, type: props.attribute.type}; 15 | this.handleCancel = this.handleCancel.bind(this); 16 | this.handleAdd = this.handleAdd.bind(this); 17 | this.onChangeName = this.onChangeName.bind(this); 18 | this.onChangeType = this.onChangeType.bind(this); 19 | } 20 | 21 | onChangeName(e) { 22 | this.setState({name: e.target.value}); 23 | } 24 | 25 | onChangeType(e) { 26 | this.setState({type: e.target.value}); 27 | } 28 | 29 | handleAdd(e) { 30 | e.preventDefault(); 31 | const error = attributeValidations({name: this.state.name, type: this.state.type}); 32 | 33 | if (Object.keys(error).length > 0) { 34 | this.setState({error}); 35 | } else { 36 | this.props.addAttribute({name: this.state.name, type: this.state.type}); 37 | } 38 | } 39 | 40 | handleCancel() { 41 | this.props.cancel(); 42 | } 43 | 44 | render() { 45 | const { buttonProps } = this.props; 46 | const attribute_types = Object.keys(dataTypes); 47 | return ( 48 |
49 |
50 |
51 | 52 | 53 |
54 |
55 |
58 |
59 |
60 |
); 61 | } 62 | } 63 | 64 | 65 | AddAttributes.defaultProps = ({ 66 | addAttribute: () => false, 67 | cancel: () => false, 68 | attribute: {}, 69 | buttonProps: {}, 70 | }); 71 | 72 | AddAttributes.propTypes = ({ 73 | addAttribute: PropTypes.func, 74 | cancel: PropTypes.func, 75 | attribute: PropTypes.object, 76 | buttonProps: PropTypes.object, 77 | }); 78 | 79 | export default AddAttributes; 80 | 81 | -------------------------------------------------------------------------------- /src/components/attributes/attr-details.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SweetAlert from 'react-bootstrap-sweetalert'; 4 | import AddAttributes from './add-atrribtues'; 5 | import { PanelBox } from '../panel/panel'; 6 | 7 | 8 | class AttributeDetails extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = {removeAlert: false, successAlert: false}; 13 | this.handleRemove = this.handleRemove.bind(this); 14 | this.remove = this.remove.bind(this); 15 | this.cancelAlert = this.cancelAlert.bind(this); 16 | this.updateAttribute = this.updateAttribute.bind(this); 17 | } 18 | 19 | handleEdit(e, val) { 20 | e.preventDefault(); 21 | this.setState({showRuleIndex: val}); 22 | } 23 | 24 | handleRemove(e, attribute, index) { 25 | e.preventDefault(); 26 | this.setState({removeAlert: true, activeAttribute: attribute, activeAttributeIndex: index}); 27 | 28 | } 29 | 30 | remove() { 31 | const { activeAttribute, activeAttributeIndex } = this.state; 32 | this.props.removeAttribute('REMOVE', activeAttribute, activeAttributeIndex); 33 | this.setState({ successAlert: true}); 34 | } 35 | 36 | cancelAlert() { 37 | this.setState({ removeAlert: false, successAlert: false, showRuleIndex: -1 }); 38 | } 39 | 40 | updateAttribute(attribute) { 41 | this.setState({ showRuleIndex: -1 }); 42 | this.props.updateAttribute('UPDATE', attribute, this.state.showRuleIndex); 43 | } 44 | 45 | removeAlert = () => { 46 | return ( 56 | You will not be able to recover the changes! 57 | ) 58 | } 59 | 60 | successAlert = () => { 61 | return ( 66 | ); 67 | } 68 | 69 | render() { 70 | 71 | const { attributes } = this.props; 72 | const { showRuleIndex } = this.state; 73 | 74 | const buttonProps = { primaryLabel: 'Save Changes', secondaryLabel: 'Cancel'}; 75 | 76 | 77 | const attrList = attributes.map((attr, index) => 78 | (
79 | 80 |
{index + 1}
81 |
{attr.name}
82 |
{attr.type}
83 |
84 | this.handleEdit(e, index)}>Edit 85 | this.handleRemove(e, attr, index)}>Remove 86 |
87 |
88 | { showRuleIndex === index && 89 | } 90 |
)); 91 | 92 | return ( 93 | {this.state.removeAlert && this.removeAlert()} 94 | {this.state.successAlert && this.successAlert()} 95 | {attrList} 96 | 97 | ); 98 | } 99 | } 100 | 101 | 102 | AttributeDetails.defaultProps = ({ 103 | attributes: [], 104 | updateAttribute: () => false, 105 | removeAttribute: () => false, 106 | }); 107 | 108 | AttributeDetails.propTypes = ({ 109 | attributes: PropTypes.array, 110 | updateAttribute: PropTypes.func, 111 | removeAttribute: PropTypes.func, 112 | }); 113 | 114 | export default AttributeDetails; -------------------------------------------------------------------------------- /src/components/attributes/attributes.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import AddAttributes from './add-atrribtues'; 4 | import AttributeDetails from './attr-details'; 5 | import ToolBar from '../toolbar/toolbar'; 6 | import Banner from '../panel/banner'; 7 | import * as Message from '../../constants/messages'; 8 | import { isContains } from '../../utils/stringutils'; 9 | 10 | class Attributes extends Component { 11 | 12 | constructor(props){ 13 | super(props); 14 | this.state={showAddAttr: false, message: Message.NO_ATTRIBUTE_MSG, searchCriteria: '', bannerflag: false }; 15 | this.handleAdd = this.handleAdd.bind(this); 16 | this.cancelAddAttribute = this.cancelAddAttribute.bind(this); 17 | this.addAttribute = this.addAttribute.bind(this); 18 | this.handleReset = this.handleReset.bind(this); 19 | this.handleSearch = this.handleSearch.bind(this); 20 | } 21 | 22 | handleSearch = (value) => { 23 | this.setState({ searchCriteria: value}) 24 | } 25 | 26 | handleAdd = () => { 27 | this.setState({showAddAttr: true, bannerflag: true }); 28 | } 29 | 30 | addAttribute = (attribute) => { 31 | this.setState({showAddAttr: false}); 32 | this.props.handleAttribute('ADD', attribute); 33 | } 34 | 35 | handleReset() { 36 | this.props.handleAttribute('RESET'); 37 | } 38 | 39 | cancelAddAttribute = () => { 40 | this.setState({ showAddAttr: false, bannerflag: false }); 41 | } 42 | 43 | filterAttribute = () => { 44 | const { searchCriteria } = this.state; 45 | const filteredAttributes = this.props.attributes.filter(att => isContains(att.name, searchCriteria) || 46 | isContains(att.type, searchCriteria)); 47 | return filteredAttributes; 48 | } 49 | 50 | render() { 51 | const { searchCriteria, bannerflag } = this.state; 52 | 53 | const buttonProps = { primaryLabel: 'Add Facts', secondaryLabel: 'Cancel'}; 54 | 55 | const filteredAttributes = searchCriteria ? this.filterAttribute() : this.props.attributes; 56 | 57 | return (
58 | 59 | { } 60 | 61 | { this.state.showAddAttr && } 62 | 63 | { } 64 | 65 | { !bannerflag && this.props.attributes.length < 1 && } 66 | 67 |
); 68 | } 69 | } 70 | 71 | Attributes.defaultProps = ({ 72 | handleAttribute: () => false, 73 | attributes: [], 74 | }); 75 | 76 | Attributes.propTypes = ({ 77 | handleAttribute: PropTypes.func, 78 | attributes: PropTypes.array, 79 | }); 80 | 81 | export default Attributes; -------------------------------------------------------------------------------- /src/components/attributes/view-attributes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ViewAttributes = ({items}) => { 5 | 6 | return (Object.keys(items).map(key => 7 | (
8 |
9 | {key} 10 |
11 |
12 | {items[key]} 13 |
14 |
)) 15 | ); 16 | }; 17 | 18 | ViewAttributes.defaultProps = { 19 | items: {}, 20 | }; 21 | 22 | ViewAttributes.propTypes = { 23 | items: PropTypes.object, 24 | }; 25 | 26 | 27 | 28 | export const ViewOutcomes = ({items}) => { 29 | 30 | return (items.map((item, ind) => 31 | (
32 |
33 | Type 34 |
35 |
36 | {item.type} 37 |
38 |
)) 39 | ); 40 | }; 41 | 42 | ViewOutcomes.defautProps = { 43 | items: [], 44 | }; 45 | 46 | ViewOutcomes.propTypes = { 47 | items: PropTypes.array, 48 | }; 49 | 50 | export default ViewAttributes; 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/button/button-groups.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ApperanceContext from '../../context/apperance-context'; 4 | 5 | const ButtonGroup = ({ buttons, onConfirm }) => { 6 | const { background } = useContext(ApperanceContext); 7 | 8 | return (
9 | {buttons.length > 0 && buttons.map(button => 10 |
11 | 13 |
)} 14 |
) 15 | }; 16 | 17 | ButtonGroup.defaultProps = { 18 | buttons: [], 19 | onConfirm: () => false 20 | }; 21 | 22 | ButtonGroup.propTypes = { 23 | buttons: PropTypes.array, 24 | onConfirm: PropTypes.func 25 | }; 26 | 27 | export default ButtonGroup; 28 | -------------------------------------------------------------------------------- /src/components/button/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Button = ({label, onConfirm, classname, type, disabled }) => { 5 | 6 | return (
7 | 8 |
); 9 | }; 10 | 11 | 12 | 13 | 14 | Button.defaultProps = { 15 | classname: 'primary-btn', 16 | onConfirm: () => undefined, 17 | label: '', 18 | type: 'button', 19 | disabled: false, 20 | }; 21 | 22 | Button.propTypes = { 23 | classname: PropTypes.string, 24 | onConfirm: PropTypes.func, 25 | label: PropTypes.string, 26 | type: PropTypes.string, 27 | disabled: PropTypes.bool, 28 | }; 29 | 30 | 31 | export default Button; -------------------------------------------------------------------------------- /src/components/button/nav-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | const NavButton = ({ classname, onConfirm, label }) => { 6 | return (
7 | 10 |
); 11 | }; 12 | 13 | NavButton.defaultProps = { 14 | classname: '', 15 | onConfirm: () => undefined, 16 | label: '', 17 | }; 18 | 19 | NavButton.propTypes = { 20 | classname: PropTypes.string, 21 | onConfirm: PropTypes.func, 22 | label: PropTypes.string 23 | }; 24 | 25 | 26 | 27 | export default NavButton; -------------------------------------------------------------------------------- /src/components/decisions/add-decision.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Panel from '../panel/panel'; 4 | import InputField from '../forms/input-field'; 5 | import SelectField from '../forms/selectmenu-field'; 6 | import Button from '../button/button'; 7 | import ButtonGroup from '../button/button-groups'; 8 | import operator from '../../data-objects/operator.json'; 9 | import decisionValidations from '../../validations/decision-validation'; 10 | import Tree from '../tree/tree'; 11 | import { has } from 'lodash/object'; 12 | import { getNodeDepthDetails, getNodeDepth } from '../../utils/treeutils'; 13 | import { transformTreeToRule } from '../../utils/transform'; 14 | import { sortBy } from 'lodash/collection'; 15 | import { validateAttribute } from '../../validations/decision-validation'; 16 | import { PLACEHOLDER } from '../../constants/data-types'; 17 | import ApperanceContext from '../../context/apperance-context'; 18 | 19 | 20 | const nodeStyle ={ 21 | shape: 'circle', 22 | shapeProps: { 23 | fill: '#1ABB9C', 24 | r: 10, 25 | }, 26 | }; 27 | 28 | const factsButton = [{ label : 'Add Facts', disable: false }, 29 | { label : 'Add All', disable: false }, 30 | { label : 'Add Any', disable: false }, 31 | { label : 'Remove', disable: false }]; 32 | 33 | const topLevelOptions = [{ label : 'All', active: false, disable: false }, 34 | { label : 'Any', active: false, disable: false }]; 35 | 36 | const outcomeOptions = [{ label : 'Add Outcome', active: false, disable: false }, 37 | { label : 'Edit Conditions', active: false, disable: false }]; 38 | 39 | class AddDecision extends Component { 40 | constructor(props) { 41 | super(props); 42 | 43 | const outcome = props.editDecision ? props.outcome : ({ value: '', error: {}, params: []}); 44 | const addAttribute = { error: {}, name: '', operator: '', value: ''}; 45 | const node = props.editDecision ? props.editCondition.node : {}; 46 | const activeNode = { index: 0, depth: 0 }; 47 | 48 | this.state = { attributes: props.attributes, 49 | outcome, 50 | addAttribute, 51 | enableTreeView: props.editDecision, 52 | enableFieldView: false, 53 | enableOutcomeView: false, 54 | node, 55 | topLevelOptions, 56 | factsButton: factsButton.map(f => ({ ...f, disable: true })), 57 | outcomeOptions: outcomeOptions.map(f => ({ ...f, disable: true })), 58 | formError: '', 59 | addPathflag: false, 60 | activeNodeDepth: [activeNode] }; 61 | this.handleAdd = this.handleAdd.bind(this); 62 | this.handleCancel = this.handleCancel.bind(this); 63 | this.onChangeNewFact = this.onChangeNewFact.bind(this); 64 | this.onChangeOutcomeValue = this.onChangeOutcomeValue.bind(this); 65 | this.handleTopNode = this.handleTopNode.bind(this); 66 | this.handleActiveNode = this.handleActiveNode.bind(this); 67 | this.handleChildrenNode = this.handleChildrenNode.bind(this); 68 | this.handleFieldCancel = this.handleFieldCancel.bind(this); 69 | this.handleOutputPanel = this.handleOutputPanel.bind(this); 70 | this.handleOutputParams = this.handleOutputParams.bind(this); 71 | this.addParams = this.addParams.bind(this); 72 | this.addPath = this.addPath.bind(this); 73 | } 74 | 75 | handleAdd(e) { 76 | e.preventDefault(); 77 | const error = decisionValidations(this.state.node, this.state.outcome); 78 | 79 | if (error.formError) { 80 | this.setState({formError: error.formError, outcome: { ...this.state.outcome, error: error.outcome }}) 81 | } else { 82 | let outcomeParams = {}; 83 | this.state.outcome.params.forEach(param => { 84 | outcomeParams[param.pkey] = param.pvalue; 85 | }) 86 | const condition = transformTreeToRule(this.state.node, this.state.outcome, outcomeParams); 87 | this.props.addCondition(condition); 88 | } 89 | } 90 | 91 | handleCancel() { 92 | this.props.cancel(); 93 | } 94 | 95 | onChangeNewFact(e, name) { 96 | const addAttribute = { ...this.state.addAttribute }; 97 | addAttribute[name] = e.target.value; 98 | this.setState({ addAttribute }); 99 | } 100 | 101 | onChangeOutcomeValue(e, type){ 102 | const outcome = { ...this.state.outcome }; 103 | outcome[type] = e.target.value; 104 | this.setState({ outcome }); 105 | } 106 | 107 | addParams() { 108 | const { outcome } = this.state; 109 | const newParams = outcome.params.concat({ pkey: '', pvalue: '' }); 110 | this.setState({ outcome: { ...outcome, params: newParams }}); 111 | } 112 | 113 | addPath() { 114 | this.setState({ addPathflag: true }); 115 | } 116 | 117 | handleOutputParams(e, type, index){ 118 | const { outcome } = this.state; 119 | const params = [ ...outcome.params ]; 120 | const newParams = params.map((param, ind) => { 121 | if (index === ind) { 122 | if (type === 'pkey') { 123 | return { ...param, pkey: e.target.value }; 124 | } else { 125 | return { ...param, pvalue: e.target.value }; 126 | } 127 | } 128 | return param; 129 | }); 130 | this.setState({outcome: { ...outcome, params: newParams }}); 131 | } 132 | 133 | handleTopNode(value) { 134 | let parentNode = { ...this.state.node}; 135 | const activeNode = { index: 0, depth: 0 }; 136 | if (has(parentNode, 'name')) { 137 | parentNode.name = value === 'All' ? 'all' : 'any'; 138 | } else { 139 | parentNode = { name: value === 'All' ? 'all' : 'any', nodeSvgShape: nodeStyle, children: [] }; 140 | } 141 | const topLevelOptions = this.state.topLevelOptions.map(option => { 142 | if (option.label === value) { 143 | return { ...option, active: true}; 144 | } 145 | return { ...option, active: false}; 146 | }) 147 | 148 | const factsButton = this.state.factsButton.map(button => ({ ...button, disable: false })); 149 | const outcomeOptions = this.state.outcomeOptions.map(button => ({ ...button, disable: false })); 150 | 151 | this.setState({ enableTreeView: true, topNodeName: value, node: parentNode, 152 | activeNodeDepth: [activeNode], topLevelOptions, factsButton, outcomeOptions }); 153 | } 154 | 155 | mapNodeName(val) { 156 | const node = {}; 157 | const { addAttribute: { name, operator, value, path }, attributes } = this.state; 158 | if (val === 'Add All' || val === 'Add Any') { 159 | node['name'] = val === 'Add All' ? 'all' : 'any'; 160 | node['nodeSvgShape'] = nodeStyle; 161 | node['children'] = []; 162 | } else { 163 | node['name'] = name; 164 | let factValue = value.trim(); 165 | const attProps = attributes.find(att => att.name === name); 166 | if (attProps.type === 'number') { 167 | factValue = Number(value.trim()); 168 | } 169 | let fact = { [operator]: factValue }; 170 | if (path) { 171 | fact['path'] = `.${path}`; 172 | } 173 | node['attributes'] = { ...fact }; 174 | } 175 | return node; 176 | } 177 | 178 | handleChildrenNode(value) { 179 | let factOptions = [ ...factsButton]; 180 | if (value === 'Add Facts') { 181 | this.setState({enableFieldView: true}); 182 | } else { 183 | const { activeNodeDepth, node, attributes } = this.state; 184 | const addAttribute = { error: {}, name: '', operator: '', value: ''}; 185 | if (value === 'Add fact node') { 186 | const error = validateAttribute(this.state.addAttribute, attributes); 187 | if (Object.keys(error).length > 0 ) { 188 | let addAttribute = this.state.addAttribute; 189 | addAttribute.error = error; 190 | this.setState({addAttribute}); 191 | return undefined; 192 | } 193 | } 194 | if (activeNodeDepth && node) { 195 | const newNode = { ...node }; 196 | 197 | const getActiveNode = (pNode, depthIndex) => pNode[depthIndex]; 198 | 199 | let activeNode = newNode; 200 | const cloneDepth = value === 'Remove' ? activeNodeDepth.slice(0, activeNodeDepth.length -1 ): [ ...activeNodeDepth ] 201 | cloneDepth.forEach(nodeDepth => { 202 | if (nodeDepth.depth !== 0) { 203 | activeNode = getActiveNode(activeNode.children, nodeDepth.index); 204 | } 205 | }); 206 | const childrens = activeNode['children'] || []; 207 | if (value !== 'Remove') { 208 | activeNode['children'] = childrens.concat(this.mapNodeName(value)); 209 | } else { 210 | const lastNode = activeNodeDepth[activeNodeDepth.length - 1]; 211 | childrens.splice(lastNode.index, 1); 212 | factOptions = this.state.factsButton.map(button => 213 | ({ ...button, disable: true })); 214 | } 215 | 216 | this.setState({node: newNode, enableFieldView: false, addAttribute, factsButton: factOptions}); 217 | } 218 | } 219 | } 220 | 221 | 222 | handleActiveNode(node) { 223 | const depthArr = getNodeDepthDetails(node); 224 | const sortedArr = sortBy(depthArr, 'depth'); 225 | 226 | const factsNodemenu = this.state.factsButton.map(button => { 227 | if (button.label !== 'Remove') { 228 | return { ...button, disable: true }; 229 | } 230 | return { ...button, disable: false }; 231 | }); 232 | 233 | const parentNodeMenu = this.state.factsButton.map(button => { 234 | if (sortedArr.length < 1 && button.label === 'Remove') { 235 | return { ...button, disable: true }; 236 | } 237 | return { ...button, disable: false }; 238 | }); 239 | 240 | const facts = node.name === 'all' || node.name === 'any' ? parentNodeMenu : factsNodemenu; 241 | const outcomeMenus = outcomeOptions.map(option => ({ ...option, disable: false })); 242 | this.setState({ activeNodeDepth: sortedArr, factsButton: facts, outcomeOptions: outcomeMenus }); 243 | } 244 | 245 | handleFieldCancel() { 246 | const addAttribute = { error: {}, name: '', operator: '', value: ''}; 247 | this.setState({ enableFieldView: false, addAttribute }); 248 | } 249 | 250 | handleOutputPanel(value) { 251 | if(value === 'Add Outcome') { 252 | const factsOptions = this.state.factsButton.map(fact => ({ ...fact, disable: true })) 253 | const options = this.state.outcomeOptions.map(opt => { 254 | if(opt.label === 'Add Outcome') { 255 | return { ...opt, active: true }; 256 | } 257 | return { ...opt, active: false }; 258 | }); 259 | this.setState({ enableOutcomeView: true, enableTreeView: false, 260 | enableFieldView: false, outcomeOptions: options, factsButton: factsOptions }); 261 | } 262 | if(value === 'Edit Conditions') { 263 | const options = this.state.outcomeOptions.map(opt => { 264 | if(opt.label === 'Edit Conditions') { 265 | return { ...opt, active: true }; 266 | } 267 | return { ...opt, active: false }; 268 | }); 269 | this.setState({ enableOutcomeView: false, enableTreeView: true, enableFieldView: false, outcomeOptions: options }); 270 | } 271 | } 272 | 273 | topPanel() { 274 | const { topLevelOptions, factsButton, outcomeOptions } = this.state; 275 | 276 | return (
277 |
Step 1: Add Toplevel
278 |
Step 2: Add / Remove facts
279 |
Step 3: Add Outcome
280 |
) 281 | } 282 | 283 | fieldPanel() { 284 | const { attributes, addAttribute, addPathflag} = this.state; 285 | const attributeOptions = attributes.map(attr => attr.name); 286 | const attribute = addAttribute.name && attributes.find(attr => attr.name === addAttribute.name); 287 | const operatorOptions = attribute && operator[attribute.type]; 288 | const { background } = this.context; 289 | 290 | const placeholder = addAttribute.operator === 'contains' || addAttribute.operator === 'doesNotContain' ? 291 | PLACEHOLDER['string'] : PLACEHOLDER[attribute.type] 292 | 293 | return ( 294 | 295 |
296 |
297 | Add Path 298 |
299 |
300 | 301 |
302 |
this.onChangeNewFact(e, 'name')} 303 | value={addAttribute.name} error={addAttribute.error.name} label="Facts"/>
304 |
this.onChangeNewFact(e, 'operator')} 305 | value={addAttribute.operator} error={addAttribute.error.operator} label="Operator"/>
306 |
this.onChangeNewFact(value, 'value')} value={addAttribute.value} 307 | error={addAttribute.error.value} label="Value" placeholder={placeholder}/>
308 |
309 | 310 | { addPathflag &&
311 |
312 | {/* this.onChangeNewFact(value, 'path')} value={addAttribute.path} 313 | label="Path" placeholder={"Enter path value - dont give prefix ' . ' "}/> */} 314 | this.onChangeNewFact(e, 'path')} 315 | value={addAttribute.path} label="Path"/> 316 |
317 |
} 318 | 319 |
320 |
323 |
) 324 | } 325 | 326 | outputPanel() { 327 | const { outcome } = this.state; 328 | const { editDecision } = this.props; 329 | const { background } = this.context; 330 | 331 | return ( 332 |
333 |
334 | Add Params 335 |
336 |
337 |
338 |
339 | this.onChangeOutcomeValue(value, 'value')} value={outcome.value} 340 | error={outcome.error && outcome.error.value} label="Type" readOnly={editDecision}/> 341 |
342 |
343 |
344 | { outcome.params.length > 0 && outcome.params.map((param, ind) => 345 | (
346 | this.handleOutputParams(value, 'pkey', ind)} value={param.pkey} label="key" /> 347 | this.handleOutputParams(value, 'pvalue', ind)} value={param.pvalue} label="Value" /> 348 |
)) } 349 |
350 |
) 351 | } 352 | 353 | treePanel() { 354 | const { node } = this.state; 355 | const depthCount = getNodeDepth(node); 356 | 357 | return( 358 | 359 | ) 360 | } 361 | 362 | 363 | addPanel() { 364 | const { enableTreeView, enableFieldView, enableOutcomeView } = this.state; 365 | 366 | return (
367 | {this.topPanel()} 368 | {enableFieldView && this.fieldPanel()} 369 | {enableOutcomeView && this.outputPanel()} 370 | {enableTreeView && this.treePanel()} 371 |
); 372 | 373 | } 374 | 375 | render() { 376 | const { buttonProps } = this.props; 377 | return ( 378 |
379 |
380 | {this.addPanel()} 381 | {this.state.formError &&

{this.state.formError}

} 382 |
383 |
386 | 387 |
388 |
389 | ); 390 | } 391 | } 392 | 393 | AddDecision.contextType = ApperanceContext; 394 | 395 | AddDecision.defaultProps = ({ 396 | addCondition: () => false, 397 | cancel: () => false, 398 | attribute: {}, 399 | buttonProps: {}, 400 | attributes: [], 401 | outcome: {}, 402 | editDecision: false, 403 | editCondition: {} 404 | }); 405 | 406 | AddDecision.propTypes = ({ 407 | addCondition: PropTypes.func, 408 | cancel: PropTypes.func, 409 | attribute: PropTypes.object, 410 | buttonProps: PropTypes.object, 411 | attributes: PropTypes.array, 412 | outcome: PropTypes.object, 413 | editDecision: PropTypes.bool, 414 | editCondition: PropTypes.object 415 | }); 416 | 417 | 418 | export default AddDecision; -------------------------------------------------------------------------------- /src/components/decisions/decision-details.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Tree from '../tree/tree'; 4 | import { PanelBox } from '../panel/panel'; 5 | import 'font-awesome/css/font-awesome.min.css'; 6 | import SweetAlert from 'react-bootstrap-sweetalert'; 7 | import { transformRuleToTree } from '../../utils/transform'; 8 | import ViewAttribute from '../attributes/view-attributes'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | import { faPenToSquare, faTrash } from '@fortawesome/free-solid-svg-icons'; 11 | 12 | class DecisionDetails extends Component { 13 | 14 | static getDerivedStateFromProps(props, state) { 15 | if (Object.keys(props.outcomes).length !== state.showCase.length) { 16 | const showCase = Object.keys(props.outcomes).map((key, index) => { 17 | return ({case: false, edit: false, index }); 18 | }); 19 | return { showCase }; 20 | } 21 | return null; 22 | } 23 | 24 | constructor(props) { 25 | super(props); 26 | const showCase = Object.keys(props.outcomes).map((key, index) => { 27 | return ({case: false, edit: false, index}); 28 | }) 29 | 30 | this.state = { showCase, submitAlert: false, removeAlert:false, successAlert: false, removeDecisionAlert: false}; 31 | this.handleExpand = this.handleExpand.bind(this); 32 | this.handleRemoveCondition = this.handleRemoveCondition.bind(this); 33 | this.handleRemoveConditions = this.handleRemoveConditions.bind(this); 34 | this.editCondition = this.editCondition.bind(this); 35 | this.cancelAlert = this.cancelAlert.bind(this); 36 | this.removeCase = this.removeCase.bind(this); 37 | this.removeDecisions = this.removeDecisions.bind(this); 38 | } 39 | 40 | 41 | handleEdit(e, val) { 42 | e.preventDefault(); 43 | this.setState({showRuleIndex: val}); 44 | } 45 | 46 | editCondition(e, decisionIndex) { 47 | e.preventDefault(); 48 | this.props.editCondition(decisionIndex); 49 | } 50 | 51 | handleExpand(e, index) { 52 | e.preventDefault(); 53 | const cases = [...this.state.showCase]; 54 | let updateCase = cases[index]; 55 | updateCase = { ...updateCase, case: !updateCase.case} 56 | cases[index] = { ...updateCase }; 57 | this.setState({ showCase: cases }); 58 | } 59 | 60 | handleRemoveCondition(e, decisionIndex) { 61 | e.preventDefault(); 62 | this.setState({ removeAlert: true, removeDecisionIndex: decisionIndex }); 63 | } 64 | 65 | handleRemoveConditions(e, outcome) { 66 | e.preventDefault(); 67 | this.setState({ removeDecisionAlert: true, removeOutcome: outcome }); 68 | } 69 | 70 | cancelAlert = () => { 71 | this.setState({ removeAlert: false, successAlert: false, removeDecisionAlert: false }); 72 | } 73 | 74 | removeCase = () => { 75 | this.props.removeCase(this.state.removeDecisionIndex); 76 | this.setState({ removeAlert: false, successAlert: true, successMsg: 'Selected condition is removed' }); 77 | } 78 | 79 | removeDecisions = () => { 80 | this.props.removeDecisions(this.state.removeOutcome); 81 | this.setState({ removeDecisionAlert: false, successAlert: true, successMsg: 'Selected conditions are removed', removeOutcome: ''}); 82 | } 83 | 84 | removeCaseAlert = () => { 85 | return ( 95 | You will not be able to recover the changes! 96 | ) 97 | } 98 | 99 | removeDecisionAlert = () => { 100 | return ( 110 | You will not be able to recover the changes! 111 | ) 112 | } 113 | 114 | successAlert = () => { 115 | return ( 120 | ); 121 | } 122 | 123 | alert = () => { 124 | return (
125 | {this.state.removeAlert && this.removeCaseAlert()} 126 | {this.state.removeDecisionAlert && this.removeDecisionAlert()} 127 | {this.state.successAlert && this.successAlert()} 128 |
); 129 | } 130 | 131 | renderConditions = (conditions, decisionIndex) => { 132 | 133 | const transformedData = transformRuleToTree(conditions); 134 | 135 | return (
136 | { transformedData && transformedData.map((data, caseIndex) => (
137 |
138 |
this.editCondition(e, data.index)}>
139 |
this.handleRemoveCondition(e, data.index))}>
140 |
141 | 142 | { data.event.params &&
143 |

Params

144 | 145 |
} 146 |
))} 147 |
) 148 | } 149 | 150 | render() { 151 | const { outcomes } = this.props; 152 | const { showCase} = this.state; 153 | 154 | const conditions = Object.keys(outcomes).map((key, index) => 155 | (
156 | 157 |
{index + 1}
158 |
{String(key)}
159 |
conditions {outcomes[key].length}
160 |
161 | this.handleExpand(e, index)}> { showCase[index].case ? 'Collapse' : 'View Conditions' } 162 | this.handleRemoveConditions(e, String(key)))}>Remove 163 |
164 |
165 | 166 | { showCase[index].case && this.renderConditions(outcomes[key], index)} 167 |
)); 168 | 169 | return (
170 | { this.alert() } 171 | { conditions } 172 |
); 173 | } 174 | } 175 | 176 | DecisionDetails.defaultProps = ({ 177 | decisions: [], 178 | editCondition: () => false, 179 | removeCase: () => false, 180 | removeDecisions: () => false, 181 | outcomes: {}, 182 | }); 183 | 184 | DecisionDetails.propTypes = ({ 185 | decisions: PropTypes.array, 186 | editCondition: PropTypes.func, 187 | removeCase: PropTypes.func, 188 | removeDecisions: PropTypes.func, 189 | outcomes: PropTypes.object, 190 | }); 191 | 192 | export default DecisionDetails; -------------------------------------------------------------------------------- /src/components/decisions/decision.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ToolBar from '../toolbar/toolbar'; 4 | import AddDecision from './add-decision'; 5 | import DecisionDetails from './decision-details'; 6 | import Banner from '../panel/banner'; 7 | import * as Message from '../../constants/messages'; 8 | import { transformRuleToTree } from '../../utils/transform'; 9 | import { isContains } from '../../utils/stringutils'; 10 | 11 | class Decision extends Component { 12 | 13 | constructor(props){ 14 | super(props); 15 | this.state={showAddRuleCase: false, 16 | searchCriteria: '', 17 | editCaseFlag: false, 18 | editCondition: [], 19 | message: Message.NO_DECISION_MSG, 20 | decisions: props.decisions || [], 21 | bannerflag: false }; 22 | this.handleAdd = this.handleAdd.bind(this); 23 | this.updateCondition = this.updateCondition.bind(this); 24 | this.editCondition = this.editCondition.bind(this); 25 | this.addCondition = this.addCondition.bind(this); 26 | this.removeCase = this.removeCase.bind(this); 27 | this.cancelAddAttribute = this.cancelAddAttribute.bind(this); 28 | this.removeDecisions = this.removeDecisions.bind(this); 29 | this.handleReset = this.handleReset.bind(this); 30 | this.handleSearch = this.handleSearch.bind(this); 31 | } 32 | 33 | handleSearch = (value) => { 34 | this.setState({ searchCriteria: value}) 35 | } 36 | 37 | handleAdd = () => { 38 | this.setState({showAddRuleCase: true, bannerflag: true }); 39 | } 40 | 41 | cancelAddAttribute = () => { 42 | this.setState({ showAddRuleCase: false, editCaseFlag: false, bannerflag: false }); 43 | } 44 | 45 | editCondition(decisionIndex) { 46 | const decision = this.props.decisions[decisionIndex]; 47 | const editCondition = transformRuleToTree(decision); 48 | let outputParams = []; 49 | if (decision.event.params && Object.keys(decision.event.params).length > 0) { 50 | outputParams = Object.keys(decision.event.params).map(key => ({ pkey: key, pvalue: decision.event.params[key] })) 51 | } 52 | 53 | this.setState({ editCaseFlag: true, editCondition, 54 | editDecisionIndex: decisionIndex, 55 | editOutcome: { value: decision.event.type, params: outputParams }}); 56 | } 57 | 58 | addCondition(condition) { 59 | this.props.handleDecisions('ADD', { condition }); 60 | this.setState({ showAddRuleCase: false }); 61 | } 62 | 63 | updateCondition(condition) { 64 | this.props.handleDecisions('UPDATE', { condition, 65 | decisionIndex: this.state.editDecisionIndex }); 66 | this.setState({ editCaseFlag: false }); 67 | } 68 | 69 | removeCase(decisionIndex) { 70 | this.props.handleDecisions('REMOVECONDITION', { decisionIndex}); 71 | } 72 | 73 | removeDecisions(outcome) { 74 | this.props.handleDecisions('REMOVEDECISIONS', { outcome}); 75 | } 76 | 77 | handleReset() { 78 | this.props.handleDecisions('RESET'); 79 | } 80 | 81 | filterOutcomes = () => { 82 | const { searchCriteria } = this.state; 83 | const { outcomes } = this.props; 84 | let filteredOutcomes = {}; 85 | Object.keys(outcomes).forEach((key) => { 86 | if (isContains(key, searchCriteria)) { 87 | filteredOutcomes[key] = outcomes[key]; 88 | } 89 | }); 90 | return filteredOutcomes; 91 | } 92 | 93 | 94 | render() { 95 | const { searchCriteria, bannerflag } = this.state; 96 | const buttonProps = { primaryLabel: 'Add Rulecase', secondaryLabel: 'Cancel'}; 97 | const editButtonProps = { primaryLabel: 'Edit Rulecase', secondaryLabel: 'Cancel'}; 98 | const filteredOutcomes = searchCriteria ? this.filterOutcomes() : this.props.outcomes; 99 | const { outcomes } = this.props; 100 | 101 | return (
102 | 103 | { } 104 | 105 | { this.state.showAddRuleCase && } 106 | 107 | { this.state.editCaseFlag && } 109 | 110 | 111 | 112 | { !bannerflag && Object.keys(outcomes).length < 1 && } 113 |
); 114 | } 115 | } 116 | 117 | Decision.defaultProps = ({ 118 | handleDecisions: () => false, 119 | submit: () => false, 120 | reset: () => false, 121 | decisions: [], 122 | attributes: [], 123 | outcomes: {}, 124 | }); 125 | 126 | Decision.propTypes = ({ 127 | handleDecisions: PropTypes.func, 128 | submit: PropTypes.func, 129 | reset: PropTypes.func, 130 | decisions: PropTypes.array, 131 | attributes: PropTypes.array, 132 | outcomes: PropTypes.object, 133 | }); 134 | 135 | export default Decision; -------------------------------------------------------------------------------- /src/components/error/ruleset-error.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Notification from '../notification/notification'; 3 | import { RULE_ERROR } from '../../constants/messages'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class RuleErrorBoundary extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { hasError: false }; 11 | } 12 | 13 | static getDerivedStateFromError() { 14 | this.setState({ hasError: true }); 15 | } 16 | 17 | render() { 18 | return ( 19 | {this.state.hasError && 20 | } 21 | {this.props.children} 22 | ) 23 | } 24 | } 25 | 26 | RuleErrorBoundary.defaultProps = { 27 | children: undefined, 28 | }; 29 | 30 | RuleErrorBoundary.propTypes = { 31 | children: PropTypes.any, 32 | }; 33 | 34 | export default RuleErrorBoundary; -------------------------------------------------------------------------------- /src/components/footer/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const FooterLinks = (props) => { 5 | return ( 6 | props.links && props.links.map(link => ( 7 |
8 | {link.src && 9 | {link.label} 10 | } 11 | {!link.src && 12 | {link.label} 13 | } 14 |
15 | )) 16 | ) 17 | }; 18 | 19 | FooterLinks.defaultProps = { 20 | links: [], 21 | }; 22 | 23 | FooterLinks.propTypes = { 24 | links: PropTypes.array 25 | }; 26 | 27 | export default FooterLinks; 28 | 29 | -------------------------------------------------------------------------------- /src/components/forms/input-field.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const InputField = (props) => { 5 | const {label, onChange, error, value, required, readOnly, placeholder} = props; 6 | const [fieldValue, setFieldValue] = useState(value); 7 | 8 | let errorClass = error ? 'error': undefined; 9 | let readOnlyClass = readOnly ? 'readOnly': undefined; 10 | 11 | const change = (e) => { 12 | setFieldValue(e.target.value); 13 | onChange(e); 14 | if (required && e.target.value) { 15 | errorClass = undefined; 16 | } 17 | } 18 | 19 | return (
20 | {label && } 21 | 22 |
); 23 | }; 24 | 25 | 26 | InputField.defaultProps = { 27 | label: undefined, 28 | onChange: () => undefined, 29 | error: undefined, 30 | value: '', 31 | required: false, 32 | readOnly: false, 33 | placeholder: '', 34 | }; 35 | 36 | InputField.propTypes = { 37 | label: PropTypes.string, 38 | onChange: PropTypes.func, 39 | error: PropTypes.string, 40 | value: PropTypes.any, 41 | required: PropTypes.bool, 42 | readOnly: PropTypes.bool, 43 | placeholder: PropTypes.string, 44 | }; 45 | 46 | 47 | export default InputField; -------------------------------------------------------------------------------- /src/components/forms/selectmenu-field.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SelectField = ({label, onChange, error, required, options, value, readOnly }) => { 5 | 6 | const [fieldValue, setFieldValue] = useState(value); 7 | 8 | let errorClass = error ? 'error': undefined; 9 | let readOnlyClass = readOnly ? 'readOnly': undefined; 10 | 11 | const change = (e) => { 12 | setFieldValue(e.target.value); 13 | onChange(e); 14 | if (required && e.target.value) { 15 | errorClass = undefined; 16 | } 17 | } 18 | 19 | return (
20 | {label && } 21 | 29 |
); 30 | }; 31 | 32 | 33 | SelectField.defaultProps = { 34 | label: undefined, 35 | onChange: () => undefined, 36 | error: undefined, 37 | required: false, 38 | options: [], 39 | value: '', 40 | readOnly: false, 41 | }; 42 | 43 | SelectField.propTypes = { 44 | label: PropTypes.string, 45 | onChange: PropTypes.func, 46 | error: PropTypes.string, 47 | required: PropTypes.bool, 48 | options: PropTypes.array, 49 | value: PropTypes.string, 50 | readOnly: PropTypes.bool, 51 | }; 52 | 53 | 54 | export default SelectField; -------------------------------------------------------------------------------- /src/components/loader/loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BeatLoader from "react-spinners/BeatLoader"; 3 | 4 | const Loader = () => { 5 | return (
6 | 11 |
); 12 | } 13 | 14 | export default Loader; -------------------------------------------------------------------------------- /src/components/navigation/navigation-link.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { createHashHistory } from 'history'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | 6 | const NavLinks = (props) => { 7 | const { links } = props; 8 | const [visible, setVisible] = useState({0: true}); 9 | const history = createHashHistory(); 10 | 11 | 12 | const enableSublinks = (e, index, navigate) => { 13 | e.preventDefault(); 14 | setVisible({[index]: !visible[index]}); 15 | if (navigate) { 16 | history.push(navigate); 17 | } 18 | 19 | } 20 | 21 | return (links.map((link, index) => ( 22 | ))); 28 | }; 29 | 30 | const NavParentLink = ({ link, onConfirm, index, visible }) => { 31 | return ( 32 |
  • onConfirm(e, index, link.navigate)}> 33 | 34 | 35 | {link.fontIcons && } 36 | {link.name} 37 | 38 |
  • ); 39 | }; 40 | 41 | NavParentLink.defaultProps = { 42 | link: {}, 43 | onConfirm: () => undefined, 44 | index: 0, 45 | visible: false, 46 | }; 47 | 48 | NavParentLink.propTypes = { 49 | link: PropTypes.object, 50 | onConfirm: PropTypes.func, 51 | index: PropTypes.number, 52 | visible: PropTypes.bool, 53 | }; 54 | 55 | 56 | const NavSubLink = ({ sublinks, visible, onConfirm, activeIndex }) => { 57 | 58 | const [active, setActive] = useState(sublinks[activeIndex]); 59 | 60 | const handleClick = (e, link) => { 61 | e.preventDefault(); 62 | setActive(link); 63 | onConfirm(link); 64 | } 65 | 66 | return (sublinks.map(link => 67 | ())); 74 | } 75 | 76 | export default NavLinks; -------------------------------------------------------------------------------- /src/components/navigation/navigation-panel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import NavLinks from './navigation-link'; 3 | import PropTypes from 'prop-types'; 4 | import { createHashHistory } from 'history'; 5 | import FooterLinks from '../footer/footer'; 6 | import footerLinks from '../../data-objects/footer-links.json'; 7 | import AppearanceContext from '../../context/apperance-context'; 8 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 9 | import { faBars, faSquarePlus, faCloudArrowUp, faSliders } from '@fortawesome/free-solid-svg-icons'; 10 | 11 | const navmenu = [{ name: 'Create Rules', navigate: './create-ruleset', iconClass: "icon", fontIcons: faSquarePlus, linkClass: 'navmenu'}, 12 | { name: 'Upload Rules', navigate: './home', iconClass: "icon", fontIcons: faCloudArrowUp, linkClass: 'navmenu' }, 13 | { name: 'Appearance', navigate: './appearance', iconClass: "icon", fontIcons: faSliders, linkClass: 'navmenu'} ]; 14 | class NavigationPanel extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = {links: []}; 19 | this.handleNavLink = this.handleNavLink.bind(this); 20 | this.handleNavBtn = this.handleNavBtn.bind(this); 21 | } 22 | 23 | handleNavBtn() { 24 | const history = createHashHistory(); 25 | history.push('./create-ruleset'); 26 | } 27 | 28 | handleNavLink(name) { 29 | const history = createHashHistory(); 30 | this.props.setActiveRulesetIndex(name); 31 | history.push('./ruleset'); 32 | 33 | } 34 | 35 | render() { 36 | const { closedState, loggedIn } = this.props; 37 | let rulesetLink = this.props.rulenames.length > 0 ? 38 | [{ name: 'Ruleset', sublinks: this.props.rulenames, iconClass:"rules-icon", linkClass: 'link-heading'}] : []; 39 | 40 | rulesetLink = rulesetLink.concat(navmenu); 41 | 42 | let sideNav = loggedIn && closedState ? 'open' : 'closed'; 43 | 44 | let appctx = this.context; 45 | 46 | return ( 47 |
    48 |
    49 | { e.preventDefault(); this.props.updateState(sideNav)}}> 50 | 51 | 52 |
    53 | {!closedState &&
    54 |
    55 | 56 |
    57 |
    58 | 59 |
    60 |
    61 | } 62 |
    63 | ) 64 | } 65 | } 66 | 67 | NavigationPanel.contextType = AppearanceContext; 68 | 69 | NavigationPanel.defaultProps = { 70 | closedState: false, 71 | rulenames: [], 72 | setActiveRulesetIndex: () => false, 73 | loggedIn: false, 74 | updateState: () => false, 75 | activeIndex: 0, 76 | }; 77 | 78 | NavigationPanel.propTypes = { 79 | closedState: PropTypes.bool, 80 | rulenames: PropTypes.array, 81 | setActiveRulesetIndex: PropTypes.func, 82 | loggedIn: PropTypes.bool, 83 | updateState: PropTypes.func, 84 | activeIndex: PropTypes.number, 85 | } 86 | 87 | export default NavigationPanel; -------------------------------------------------------------------------------- /src/components/notification/notification.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import 'react-notifications/lib/notifications.css'; 4 | import { NotificationContainer, NotificationManager } from 'react-notifications'; 5 | 6 | const Notification = (props) => { 7 | 8 | useEffect(() => { 9 | NotificationManager[props.type](props.body, props.heading, 5000); 10 | }) 11 | 12 | return (); 13 | 14 | }; 15 | 16 | Notification.defaultProps = { 17 | body: '', 18 | heading: '', 19 | type: 'warning', 20 | }; 21 | 22 | Notification.propTypes = { 23 | body: PropTypes.string, 24 | heading: PropTypes.string, 25 | type: PropTypes.string, 26 | }; 27 | 28 | export default memo(Notification); -------------------------------------------------------------------------------- /src/components/panel/banner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Button from '../button/button'; 4 | 5 | class Banner extends Component { 6 | 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | getButtonClass() { 12 | const { message } = this.props; 13 | switch (message.type) { 14 | case 'warning-panel': 15 | return 'btn-warning'; 16 | case 'submit-panel': 17 | return 'btn-primary'; 18 | default: 19 | return 'btn-dark'; 20 | } 21 | 22 | } 23 | 24 | render() { 25 | const { message } = this.props; 26 | const btnClass = this.getButtonClass(); 27 | return (
    28 |
    29 | 30 |
    31 |
    {message.header}
    32 |

    {message.body}

    33 |
    34 | {message.buttonProps &&
    } 35 |
    36 |
    37 |
    ) 38 | } 39 | } 40 | 41 | Banner.defaultProps = { 42 | ruleset: {}, 43 | message: {}, 44 | onConfirm: () => false, 45 | }; 46 | 47 | Banner.propTypes = { 48 | ruleset: PropTypes.object, 49 | message: PropTypes.object, 50 | onConfirm: PropTypes.func, 51 | } 52 | 53 | export default Banner; 54 | -------------------------------------------------------------------------------- /src/components/panel/panel.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import PropTypes from 'prop-types'; 3 | import { TitleIcon } from '../title/page-title'; 4 | import ApperanceContext from '../../context/apperance-context'; 5 | 6 | const Panel = (props) => { 7 | 8 | return (
    9 | {props.title &&

    {props.title}

    } 10 | {props.children} 11 |
    ); 12 | }; 13 | 14 | 15 | 16 | 17 | Panel.defaultProps = { 18 | title: undefined, 19 | children: {}, 20 | }; 21 | 22 | Panel.propTypes = { 23 | title: PropTypes.string, 24 | children: PropTypes.any, 25 | }; 26 | 27 | 28 | export const PanelBox = (props) => { 29 | 30 | return (
    31 | {props.children} 32 |
    ); 33 | } 34 | 35 | 36 | PanelBox.defaultProps = { 37 | children: {}, 38 | className: '', 39 | }; 40 | 41 | PanelBox.propTypes = { 42 | children: PropTypes.any, 43 | className: PropTypes.string, 44 | }; 45 | 46 | export const TitlePanel = (props) => { 47 | 48 | let appTheme = useContext(ApperanceContext); 49 | 50 | return (
    51 |
    52 | {props.titleClass && 53 |
    54 | {props.titleClass && } 55 |
    } 56 |

    {props.title}

    57 |
    58 | {props.children} 59 |
    ); 60 | } 61 | 62 | 63 | TitlePanel.defaultProps = { 64 | children: {}, 65 | titleClass: '', 66 | title: '', 67 | }; 68 | 69 | TitlePanel.propTypes = { 70 | children: PropTypes.any, 71 | titleClass: PropTypes.string, 72 | title: PropTypes.string, 73 | }; 74 | 75 | 76 | export default Panel; -------------------------------------------------------------------------------- /src/components/search/search.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Search = ({onChange}) => { 5 | 6 | const [search, setSearch] = useState(''); 7 | 8 | const handleSearch = (e) => { 9 | setSearch(e.target.value); 10 | onChange(e.target.value); 11 | }; 12 | 13 | return (
    14 | 15 |
    ); 16 | }; 17 | 18 | Search.defaultProps = ({ 19 | value: '', 20 | onChange: () => false, 21 | }); 22 | 23 | Search.propTypes = ({ 24 | value: PropTypes.string, 25 | onChange: PropTypes.func, 26 | }); 27 | 28 | export default Search; -------------------------------------------------------------------------------- /src/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | class Table extends Component { 6 | 7 | render() { 8 | const { columns } = this.props; 9 | 10 | return ( 11 | 12 | 13 | {columns.map(value => ( 14 | 15 | ))} 16 | 17 | {this.props.children} 18 | 19 |
    {value}
    ) 20 | } 21 | } 22 | 23 | Table.defaultProps = ({ 24 | columns: [], 25 | children: undefined, 26 | }); 27 | 28 | Table.propTypes = ({ 29 | columns: PropTypes.array, 30 | children: PropTypes.any, 31 | }); 32 | 33 | export default Table; -------------------------------------------------------------------------------- /src/components/tabs/tabs.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Tabs = ({tabs, onConfirm, activeTab}) => { 5 | 6 | const [active, setActive] = useState(activeTab); 7 | 8 | const setActiveTab = (value) => { 9 | setActive(value); 10 | onConfirm(value); 11 | } 12 | 13 | return (
    14 | { tabs.map(tab => ( 15 |
    setActiveTab(tab.name) }> 16 | {tab.name} 17 |
    18 | )) } 19 |
    ); 20 | }; 21 | 22 | Tabs.defaultProps = ({ 23 | tabs: [], 24 | onConfirm: () => undefined, 25 | activeTab: '', 26 | }); 27 | 28 | Tabs.propTypes = ({ 29 | tabs: PropTypes.array, 30 | onConfirm: PropTypes.func, 31 | activeTab: PropTypes.string 32 | }); 33 | 34 | export default Tabs; 35 | 36 | -------------------------------------------------------------------------------- /src/components/title/page-title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | 5 | const PageTitle = ({name, titleFlag}) => { 6 | 7 | return (
    8 | {titleFlag && } 9 |

    {name}

    10 |
    ); 11 | }; 12 | 13 | PageTitle.defaultProps = { 14 | name: '', 15 | classname: '', 16 | titleFlag: false, 17 | }; 18 | 19 | PageTitle.propTypes = { 20 | name: PropTypes.string, 21 | classname: PropTypes.string, 22 | titleFlag: PropTypes.bool, 23 | } 24 | 25 | 26 | export const TitleIcon = ({iconClass}) => { 27 | 28 | return (
    29 | 30 |
    ); 31 | }; 32 | 33 | TitleIcon.defaultProps = { 34 | iconClass: '', 35 | }; 36 | 37 | TitleIcon.propTypes = { 38 | iconClass: PropTypes.string, 39 | } 40 | 41 | export default PageTitle; -------------------------------------------------------------------------------- /src/components/title/title.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import AppearanceContext from '../../context/apperance-context'; 4 | 5 | const Title = (props) => { 6 | const appctx = useContext(AppearanceContext); 7 | return ( 8 |
    9 |
    10 | {props.title} 11 |
    12 | 13 |
    14 | )}; 15 | 16 | Title.defaultProps = { 17 | title: 'Json Rule Editor', 18 | }; 19 | 20 | Title.propTypes = { 21 | title: PropTypes.string, 22 | } 23 | 24 | export default Title; -------------------------------------------------------------------------------- /src/components/toolbar/toolbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SweetAlert from 'react-bootstrap-sweetalert'; 4 | import Search from '../search/search'; 5 | import ApperanceContext from '../../context/apperance-context'; 6 | 7 | class ToolBar extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleReset = this.handleReset.bind(this); 11 | this.handleSearch = this.handleSearch.bind(this); 12 | this.reset = this.reset.bind(this); 13 | this.handleSearch = this.handleSearch.bind(this); 14 | this.state={ submitAlert: false, resetAlert:false, successAlert: false }; 15 | } 16 | 17 | handleReset() { 18 | this.setState({resetAlert: true}); 19 | } 20 | 21 | handleSearch(value) { 22 | this.props.searchTxt(value); 23 | } 24 | 25 | cancelAlert = () => { 26 | this.setState({submitAlert: false, resetAlert: false, successAlert: false}); 27 | } 28 | 29 | reset = () => { 30 | this.props.reset(); 31 | this.setState({resetAlert: false, successAlert: true, successMsg: 'Your changes are reset'}); 32 | } 33 | 34 | alert = () => { 35 | return (
    36 | {this.state.resetAlert && this.resetAlert()} 37 | {this.state.successAlert && this.successAlert()} 38 |
    ); 39 | } 40 | 41 | successAlert = () => { 42 | return ( 47 | ); 48 | } 49 | 50 | resetAlert = () => { 51 | return ( 61 | You will not be able to recover the changes! 62 | ) 63 | } 64 | 65 | 66 | render() { 67 | const { background } = this.context; 68 | return (
    69 | {this.alert()} 70 |
    71 | Add 72 |
    73 |
    74 | Reset 75 |
    76 |
    77 |
    ) 78 | } 79 | } 80 | 81 | 82 | ToolBar.contextType = ApperanceContext; 83 | 84 | ToolBar.defaultProps = ({ 85 | handleAdd: () => false, 86 | reset: () => false, 87 | searchTxt: () => false, 88 | }); 89 | 90 | ToolBar.propTypes = ({ 91 | handleAdd: PropTypes.func, 92 | reset: PropTypes.func, 93 | searchTxt: PropTypes.func, 94 | }); 95 | 96 | export default ToolBar; -------------------------------------------------------------------------------- /src/components/tree/tree-style.js: -------------------------------------------------------------------------------- 1 | const lineStyle = { 2 | stroke: '#404040', 3 | strokeWidth: 1, 4 | }; 5 | 6 | const lineStyleDark = { 7 | stroke: '#ddd', 8 | strokeWidth: 1, 9 | }; 10 | 11 | const nodeStyle = { 12 | stroke: '#73879C', 13 | strokeWidth: 1 14 | }; 15 | 16 | const nodeStyleDark = { 17 | stroke: '#1ABB9C', 18 | strokeWidth: 1 19 | }; 20 | 21 | const nameStyle = { 22 | fontFamily : '"Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif', 23 | fontSize: '13px', 24 | fontWeight: 'bold', 25 | fill: '#2A3F54', 26 | stroke: '#2A3F54', 27 | strokeWidth: 0, 28 | } 29 | 30 | const nameStyleDark = { 31 | fontFamily : '"Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif', 32 | fontSize: '13px', 33 | fontWeight: 'bold', 34 | fill: '#fff', 35 | stroke: '#fff', 36 | strokeWidth: 0, 37 | } 38 | 39 | const attributesStyle = { 40 | fontFamily : '"Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif', 41 | fontSize: '14px', 42 | fill: '#2A3F54', 43 | stroke: '#2A3F54', 44 | strokeWidth: 0, 45 | } 46 | 47 | const attributesStyleDark = { 48 | fontFamily : '"Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif', 49 | fontSize: '14px', 50 | fill: '#fff', 51 | stroke: '#fff', 52 | strokeWidth: 0, 53 | } 54 | 55 | const light = { 56 | links: lineStyle, 57 | nodes: { 58 | node: { 59 | circle: nodeStyle, 60 | rect: nodeStyle, 61 | name: nameStyle, 62 | attributes: attributesStyle, 63 | }, 64 | leafNode: { 65 | circle: nodeStyle, 66 | name: nameStyle, 67 | attributes: attributesStyle, 68 | }, 69 | }, 70 | }; 71 | 72 | const dark = { 73 | links: lineStyleDark, 74 | nodes: { 75 | node: { 76 | circle: nodeStyleDark, 77 | rect: nodeStyleDark, 78 | name: nameStyleDark, 79 | attributes: attributesStyleDark, 80 | }, 81 | leafNode: { 82 | circle: nodeStyleDark, 83 | name: nameStyleDark, 84 | attributes: attributesStyleDark, 85 | }, 86 | }, 87 | }; 88 | 89 | const TreeStyle = background => background === 'light' ? light : dark; 90 | 91 | export default TreeStyle; -------------------------------------------------------------------------------- /src/components/tree/tree.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Tree from 'react-d3-tree'; 4 | import ApperanceContext from '../../context/apperance-context'; 5 | import TreeStyle from './tree-style'; 6 | 7 | const transformDepth = (count) => { 8 | if (count < 1) { 9 | return '15em'; 10 | } else if (count < 2) { 11 | return '20em'; 12 | } else if (count < 3) { 13 | return '25em'; 14 | } else if (count < 4) { 15 | return '35em'; 16 | } else if (count < 5) { 17 | return '40em'; 18 | } else if (count < 6) { 19 | return '50em'; 20 | } else { 21 | return '60em'; 22 | } 23 | } 24 | 25 | class TreeView extends Component { 26 | constructor(props) { 27 | super(props); 28 | const { innerWidth: width } = window; 29 | this.state = {axis: { x: (width/2) - 250, y: 20}}; 30 | this.updateWindowDimensions = this.updateWindowDimensions.bind(this); 31 | this.handleClick = this.handleClick.bind(this); 32 | } 33 | 34 | componentDidMount() { 35 | window.addEventListener('resize', this.updateWindowDimensions); 36 | } 37 | 38 | componentWillUnmount() { 39 | window.removeEventListener('resize', this.updateWindowDimensions); 40 | } 41 | 42 | handleClick(e) { 43 | this.props.onConfirm(e); 44 | } 45 | 46 | 47 | updateWindowDimensions() { 48 | const { innerWidth: width } = window; 49 | this.setState({axis: { x: (width/2) - 250, y: 0}}); 50 | } 51 | 52 | render() { 53 | const heightStyle = transformDepth(this.props.count); 54 | const { background } = this.context; 55 | const nodeStyle = TreeStyle(background); 56 | return( 57 |
    58 |
    ); 60 | } 61 | } 62 | 63 | TreeView.contextType = ApperanceContext; 64 | 65 | TreeView.defaultProps = { 66 | treeData: {}, 67 | count: 0, 68 | onConfirm: () => false, 69 | }; 70 | 71 | TreeView.propTypes = { 72 | treeData: PropTypes.object, 73 | count: PropTypes.number, 74 | onConfirm: PropTypes.func, 75 | } 76 | 77 | export default TreeView; -------------------------------------------------------------------------------- /src/components/validate/validate-rules.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Panel from '../panel/panel'; 4 | import InputField from '../forms/input-field'; 5 | import SelectField from '../forms/selectmenu-field'; 6 | import Button from '../button/button'; 7 | import Table from '../table/table'; 8 | import Banner from '../panel/banner'; 9 | import * as Message from '../../constants/messages'; 10 | import { validateRuleset } from '../../validations/rule-validation'; 11 | import Loader from '../loader/loader'; 12 | import { ViewOutcomes } from '../attributes/view-attributes'; 13 | 14 | class ValidateRules extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | const conditions = props.attributes.filter(attr => attr.type !== 'object' && ({ name: attr.name, value: ''})) 19 | this.state = { attributes: [], 20 | conditions, 21 | message: Message.NO_VALIDATION_MSG, 22 | loading: false, 23 | outcomes: [], 24 | error: false, 25 | }; 26 | this.handleAttribute = this.handleAttribute.bind(this); 27 | this.handleValue = this.handleValue.bind(this); 28 | this.handleAdd = this.handleAdd.bind(this); 29 | this.validateRules = this.validateRules.bind(this); 30 | } 31 | 32 | handleAttribute(e, index) { 33 | const attribute = { ...this.state.conditions[index], name: e.target.value }; 34 | const conditions = [ ...this.state.conditions.slice(0, index), attribute, ...this.state.conditions.slice(index + 1)]; 35 | this.setState({ conditions }); 36 | } 37 | 38 | handleValue(e, index) { 39 | const attribute = { ...this.state.conditions[index], value: e.target.value }; 40 | const conditions = [ ...this.state.conditions.slice(0, index), attribute, ...this.state.conditions.slice(index + 1)]; 41 | this.setState({ conditions }); 42 | } 43 | 44 | handleAdd() { 45 | this.setState({ conditions: this.state.conditions.concat([{name: ''}])}); 46 | } 47 | 48 | validateRules(e) { 49 | e.preventDefault(); 50 | let facts = {}; 51 | const { decisions, attributes } = this.props; 52 | this.setState({loading: true}); 53 | this.state.conditions.forEach(condition => { 54 | const attrProps = attributes.find(attr => attr.name === condition.name); 55 | if (attrProps.type === 'number') { 56 | facts[condition.name] = Number(condition.value); 57 | } else if (condition.value && condition.value.indexOf(',') > -1) { 58 | facts[condition.name] = condition.value.split(','); 59 | } else { 60 | facts[condition.name] = condition.value; 61 | } 62 | }) 63 | validateRuleset(facts, decisions).then(outcomes => { 64 | this.setState({loading: false, outcomes, result: true, error: false, errorMessage: '',}); 65 | }).catch((e) => { 66 | this.setState({loading: false, error: true, errorMessage: e.error, result: true, }); 67 | }); 68 | } 69 | 70 | attributeItems = () => { 71 | const { conditions, loading, outcomes, result, error, errorMessage } = this.state; 72 | const { attributes } = this.props; 73 | const options = attributes.map(att => att.name); 74 | 75 | const formElements = conditions.map((condition, index) => 76 | ( 77 | this.handleAttribute(e, index)} 78 | value={condition.name} readOnly/> 79 | { this.handleValue(e, index)} value={condition.value} />} 80 | ) 81 | ); 82 | 83 | let message; 84 | if (result) { 85 | if (error) { 86 | message =
    Problem occured when processing the rules. Reason is {errorMessage}
    87 | } else if (outcomes && outcomes.length < 1) { 88 | message =
    No results found
    89 | } else if (outcomes && outcomes.length > 0) { 90 | message = (
    91 |

    Outcomes

    92 | 93 |
    ) 94 | } else { 95 | message = undefined; 96 | } 97 | } 98 | return ( 99 | 100 | 101 | {formElements} 102 |
    103 |
    104 |
    106 |
    107 | { loading && } 108 | { !loading && message } 109 |
    ) 110 | } 111 | 112 | render() { 113 | return ( 114 | {this.props.decisions.length < 1 && } 115 | {this.props.decisions.length > 0 && 116 | 117 |
    118 |
    119 | {this.attributeItems()} 120 |
    121 |
    122 |
    } 123 |
    ); 124 | } 125 | } 126 | 127 | ValidateRules.defaultProps = ({ 128 | attributes: [], 129 | decisions: [], 130 | }); 131 | 132 | ValidateRules.propTypes = ({ 133 | attributes: PropTypes.array, 134 | decisions: PropTypes.array, 135 | }); 136 | 137 | export default ValidateRules; -------------------------------------------------------------------------------- /src/constants/app-style.js: -------------------------------------------------------------------------------- 1 | export const THEME = { dark: { backgroundColor: '#081d31' }, 2 | light: { backgroundColor: '#fff' } 3 | }; -------------------------------------------------------------------------------- /src/constants/data-types.js: -------------------------------------------------------------------------------- 1 | export const DATA_TYPES = {STRING: 'string', BOOLEAN: 'boolean', NUMBER: 'number', ARRAY: 'array'}; 2 | 3 | export const PLACEHOLDER = { 'string': 'Enter value', 'number': 'Enter numerical value', 'array': 'Ex: item1,item2,item3'}; -------------------------------------------------------------------------------- /src/constants/messages.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const NO_CHANGES_HEADER = 'No Changes'; 4 | const NO_CHANGES_BODY = 'You havent modified this ruleset. Nothing to submit!!!' 5 | const BUTTON_PROPS_NO_CHANGES = {label: 'Generate Ruleset'}; 6 | export const NO_CHANGES_MSG = {header: NO_CHANGES_HEADER, body: NO_CHANGES_BODY, type: 'warning-panel'}; 7 | 8 | 9 | const MODIFIED_HEADER = 'Submit Ruleset'; 10 | const MODIFIED_BODY = 'You have created / modified this ruleset. Do you want to save these changes into ruleset file?' 11 | export const MODIFIED_MSG = {header: MODIFIED_HEADER, body: MODIFIED_BODY, type: 'submit-panel', buttonProps: BUTTON_PROPS_NO_CHANGES,}; 12 | 13 | 14 | const NO_ATTRIBUTE_HEADER = 'No Facts'; 15 | const NO_ATTRIBUTE_BODY = 'There is no facts available in the selected ruleset.' 16 | const BUTTON_PROPS_ATTRIBUTE = {label: 'Create Facts'}; 17 | export const NO_ATTRIBUTE_MSG = {header: NO_ATTRIBUTE_HEADER, body: NO_ATTRIBUTE_BODY, buttonProps: BUTTON_PROPS_ATTRIBUTE, type: 'warning-panel'}; 18 | 19 | 20 | const NO_DECISION_HEADER = 'No Decisions'; 21 | const NO_DECISION_BODY = 'There is no decisions available in the selected ruleset.' 22 | const BUTTON_PROPS_DECISION = {label: 'Create Decisions'}; 23 | export const NO_DECISION_MSG = {header: NO_DECISION_HEADER, body: NO_DECISION_BODY, buttonProps: BUTTON_PROPS_DECISION, type: 'warning-panel'}; 24 | 25 | 26 | const NO_VALIDATION_BODY = 'There is no decisions available in the selected ruleset to validate.' 27 | export const NO_VALIDATION_MSG = {header: NO_DECISION_HEADER, body: NO_VALIDATION_BODY, type: 'warning-panel'}; 28 | 29 | export const RULE_AVAILABLE_CREATE = { type: 'warning', heading: 'This rule name is already exist' }; 30 | 31 | export const RULE_AVAILABLE_UPLOAD = { type: 'warning', heading: 'Couldnt upload the filename ' }; 32 | 33 | export const RULE_UPLOAD_ERROR = { type: 'error', heading: 'Problem occured when uploading the files. Try again!!'}; 34 | 35 | export const RULE_ERROR = { type: 'error', heading: 'Sorry!, some problem occured. Please try again'}; -------------------------------------------------------------------------------- /src/containers/app/app-container.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Title from '../../components/title/title'; 4 | import NavigationPanel from '../../components/navigation/navigation-panel'; 5 | import AppRoutes from '../../routes/app-routes'; 6 | import PropTypes from 'prop-types'; 7 | import { updateRulesetIndex } from '../../actions/ruleset'; 8 | import { updateState } from '../../actions/app'; 9 | import { createHashHistory } from 'history'; 10 | import ApperanceContext from '../../context/apperance-context'; 11 | 12 | class ApplicationContainer extends Component { 13 | 14 | constructor(props){ 15 | super(props); 16 | const history = createHashHistory(); 17 | if (!this.props.loggedIn) { 18 | history.push('./home'); 19 | } 20 | this.toggleBackground = (value) => { 21 | const theme = { ...this.state.theme, background: value }; 22 | document.body.className = value; 23 | this.setState({ theme }); 24 | } 25 | this.state = {theme: { background: 'light', toggleBackground: this.toggleBackground }}; 26 | } 27 | 28 | componentDidMount() { 29 | document.body.className = this.state.theme.background; 30 | } 31 | 32 | componentWillUnmount() { 33 | if (this.unlisten){ 34 | this.unlisten(); 35 | } 36 | } 37 | 38 | render() { 39 | const closednav = this.props.navState !== 'open'; 40 | return( 41 | 42 | 43 | 44 | <NavigationPanel closedState={closednav} updateState={this.props.updateState} activeIndex={this.props.activeIndex} 45 | rulenames={this.props.rulenames} setActiveRulesetIndex={this.props.setActiveRulesetIndex} loggedIn={this.props.loggedIn}/> 46 | <AppRoutes closedState={closednav} loggedIn={this.props.loggedIn} appctx={this.state.theme} /> 47 | </ApperanceContext.Provider> 48 | </React.Fragment> 49 | ) 50 | } 51 | } 52 | 53 | ApplicationContainer.defaultProps = { 54 | rulenames: [], 55 | setActiveRulesetIndex: () => false, 56 | navState: undefined, 57 | activeIndex: 0, 58 | loggedIn: false, 59 | updateState: () => false, 60 | }; 61 | 62 | ApplicationContainer.propTypes = { 63 | rulenames: PropTypes.array, 64 | setActiveRulesetIndex: PropTypes.func, 65 | navState: PropTypes.string, 66 | loggedIn: PropTypes.bool, 67 | updateState: PropTypes.func, 68 | activeIndex: PropTypes.number, 69 | } 70 | 71 | 72 | const mapStateToProps = (state, ownProps) => ({ 73 | navState: state.app.navState, 74 | rulenames: state.ruleset.rulesets.map(r => r.name), 75 | loggedIn: state.app.loggedIn, 76 | activeIndex: state.ruleset.activeRuleset, 77 | ownProps 78 | }); 79 | 80 | const mapDispatchToProps = (dispatch) => ({ 81 | handleClick: () => { 82 | return false; 83 | }, 84 | setActiveRulesetIndex: (name) => dispatch(updateRulesetIndex(name)), 85 | updateState: (val) => dispatch(updateState(val)), 86 | 87 | }); 88 | 89 | export default connect(mapStateToProps, mapDispatchToProps)(ApplicationContainer); -------------------------------------------------------------------------------- /src/containers/app/appearance-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import AppearanceContext from '../../context/apperance-context'; 4 | 5 | const themes = [{ name: 'Light', value: "light", class: 'light-mode'}, 6 | { name: 'Dark', value: "dark", class: 'dark-mode' }, 7 | { name: 'Midnight Blue', value: "md-blue", class: 'mdblue-mode' }]; 8 | 9 | class AppearanceContainer extends Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | const { toggleBackground, background } = this.context; 17 | return(<div> 18 | <h3>Theme</h3> 19 | <div className="theme-container"> 20 | { themes.map(theme => (<div className="theme-icon" key={theme.value}> 21 | <span className={theme.class}></span> 22 | <div><input type="radio" name="themeMode" value={theme.value} checked={ background === theme.value } 23 | onClick={(e) => toggleBackground(e.target.value)}/>{theme.name}</div> 24 | </div>)) 25 | } 26 | </div> 27 | </div>); 28 | } 29 | 30 | } 31 | 32 | AppearanceContainer.contextType = AppearanceContext; 33 | 34 | const mapStateToProps = () => {}; 35 | 36 | const mapDispatchToProps = () => {}; 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(AppearanceContainer); -------------------------------------------------------------------------------- /src/containers/home/home-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { login } from '../../actions/app'; 5 | import { uploadRuleset } from '../../actions/ruleset'; 6 | import { TitlePanel } from '../../components/panel/panel'; 7 | import Button from '../../components/button/button'; 8 | import { createHashHistory } from 'history'; 9 | import FooterLinks from '../../components/footer/footer'; 10 | import footerLinks from '../../data-objects/footer-links.json'; 11 | import { includes } from 'lodash/collection'; 12 | import Notification from '../../components/notification/notification'; 13 | import { RULE_AVAILABLE_UPLOAD, RULE_UPLOAD_ERROR } from '../../constants/messages'; 14 | import ApperanceContext from '../../context/apperance-context'; 15 | import { faCloudArrowUp } from '@fortawesome/free-solid-svg-icons' 16 | 17 | 18 | function readFile(file, cb) { 19 | // eslint-disable-next-line no-undef 20 | var reader = new FileReader(); 21 | reader.onload = () => { 22 | try { 23 | cb(JSON.parse(reader.result), file.name); 24 | } catch (e) { 25 | cb(undefined, undefined, e.message); 26 | } 27 | } 28 | return reader.readAsText(file); 29 | } 30 | 31 | class HomeContainer extends Component { 32 | 33 | constructor(props) { 34 | super(props); 35 | this.state = { uploadedFilesCount: 0, files: [], ruleset: [], uploadError: false, fileExist: false, message: {}}; 36 | this.drop = this.drop.bind(this); 37 | this.allowDrop = this.allowDrop.bind(this); 38 | this.printFile = this.printFile.bind(this); 39 | this.handleUpload = this.handleUpload.bind(this); 40 | this.chooseDirectory = this.chooseDirectory.bind(this); 41 | } 42 | 43 | allowDrop(e) { 44 | e.preventDefault(); 45 | } 46 | 47 | printFile(file, name, error) { 48 | if (error) { 49 | this.setState({ uploadError: true, fileExist: false, message: RULE_UPLOAD_ERROR }); 50 | } else { 51 | const isFileAdded = this.state.files.some(fname => fname === name) || includes(this.props.rulenames, file.name); 52 | if (!isFileAdded) { 53 | const files = this.state.files.concat([name]); 54 | const ruleset = this.state.ruleset.concat(file); 55 | this.setState({files, ruleset, fileExist: false }); 56 | } else { 57 | const message = { ...RULE_AVAILABLE_UPLOAD, heading: RULE_AVAILABLE_UPLOAD.heading.replace('<name>', file.name) }; 58 | this.setState({ fileExist: true, message }); 59 | } 60 | } 61 | 62 | } 63 | 64 | uploadFile(items, index) { 65 | const file = items[index].getAsFile(); 66 | readFile(file, this.printFile); 67 | } 68 | 69 | uploadDirectory(item) { 70 | var dirReader = item.createReader(); 71 | const print = this.printFile; 72 | dirReader.readEntries(function(entries) { 73 | for (let j=0; j<entries.length; j++) { 74 | let subItem = entries[j]; 75 | if (subItem.isFile) { 76 | subItem.file((file) => { 77 | readFile(file, print); 78 | }); 79 | } 80 | } 81 | }); 82 | } 83 | 84 | // this method is not required. its to select files from local disk. 85 | /* chooseFile() { 86 | const file = document.getElementById("uploadFile"); 87 | if (file && file.files) { 88 | for (let i = 0; i < file.files.length; i++) { 89 | readFile(file.files[i], this.printFile); 90 | } 91 | } 92 | } */ 93 | 94 | chooseDirectory(e) { 95 | const files = e.target.files; 96 | if (files) { 97 | for (let i = 0; i < files.length; i++) { 98 | if (files[i].type === 'application/json') { 99 | readFile(files[i], this.printFile); 100 | } 101 | } 102 | } 103 | } 104 | 105 | drop(e) { 106 | e.preventDefault(); 107 | const items = e.dataTransfer.items; 108 | if (items) { 109 | for (let i = 0; i < items.length; i++) { 110 | let item = items[i].webkitGetAsEntry(); 111 | if (item.isFile) { 112 | this.uploadFile(items, i); 113 | } else if (item.isDirectory) { 114 | this.uploadDirectory(item); 115 | } 116 | } 117 | } 118 | } 119 | 120 | handleUpload() { 121 | if(this.state.ruleset.length > 0) { 122 | this.props.uploadRuleset(this.state.ruleset); 123 | this.navigate('./ruleset', 'upload'); 124 | } 125 | } 126 | 127 | navigate(location, action) { 128 | const history = createHashHistory(); 129 | this.props.login(action); 130 | history.push(location); 131 | } 132 | 133 | render() { 134 | const { fileExist, uploadError, message } = this.state; 135 | const title = this.props.loggedIn ? "Upload Rules" : "Create / Upload Rules"; 136 | const appctx = this.context; 137 | 138 | return <div className="home-container"> 139 | <div className="single-panel-container"> 140 | { (fileExist || uploadError) && <Notification body={message.body} heading={message.heading} type={message.type} /> } 141 | <TitlePanel title={title} titleClass={faCloudArrowUp}> 142 | <div className="upload-panel"> 143 | <div className={`drop-section ${appctx.background}`} onDrop={this.drop} onDragOver={this.allowDrop}> 144 | <div><label htmlFor="uploadFile">Choose Ruleset directory<input id="uploadFile" type="file" onChange={this.chooseDirectory} webkitdirectory="true" multiple/></label> or Drop Files</div> 145 | {this.state.files.length > 0 && <div className="file-drop-msg">{`${this.state.files.length} json files are dropped!`}</div>} 146 | </div> 147 | </div> 148 | <div className="btn-group"> 149 | <Button label={"Upload"} onConfirm={this.handleUpload} classname="primary-btn" type="button" /> 150 | {!this.props.loggedIn && <Button label={"Create"} onConfirm={() => this.navigate('./create-ruleset', 'create')} classname="primary-btn" type="button" disabled={this.state.files.length > 0} />} 151 | </div> 152 | </TitlePanel> 153 | </div> 154 | {!this.props.loggedIn && <div className='footer-container home-page'> 155 | <FooterLinks links={footerLinks} /> 156 | </div>} 157 | </div> 158 | } 159 | } 160 | 161 | HomeContainer.contextType = ApperanceContext; 162 | 163 | HomeContainer.propTypes = { 164 | ruleset: PropTypes.array, 165 | uploadRuleset: PropTypes.func, 166 | login: PropTypes.func, 167 | loggedIn: PropTypes.bool, 168 | rulenames: PropTypes.array, 169 | } 170 | 171 | HomeContainer.defaultProps = { 172 | rulenames: [], 173 | ruleset: [], 174 | uploadRuleset: () => false, 175 | login: () => false, 176 | loggedIn: false, 177 | } 178 | 179 | const mapStateToProps = (state) => ({ 180 | rulenames: state.ruleset.rulesets.map(r => r.name), 181 | loggedIn: state.app.loggedIn, 182 | }); 183 | 184 | const mapDispatchToProps = (dispatch) => ({ 185 | 186 | login: (action) => dispatch(login(action)), 187 | uploadRuleset: (ruleset) => dispatch(uploadRuleset(ruleset)), 188 | 189 | }); 190 | 191 | export default connect(mapStateToProps, mapDispatchToProps)(HomeContainer); -------------------------------------------------------------------------------- /src/containers/ruleset/create-ruleset-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TitlePanel } from '../../components/panel/panel'; 3 | import InputField from '../../components/forms/input-field'; 4 | import Button from '../../components//button/button'; 5 | import { connect } from 'react-redux'; 6 | import PropTypes from 'prop-types'; 7 | import { includes } from 'lodash/collection'; 8 | import { createHashHistory } from 'history'; 9 | import { addRuleset } from '../../actions/ruleset'; 10 | import Notification from '../../components/notification/notification'; 11 | import { RULE_AVAILABLE_CREATE } from '../../constants/messages'; 12 | import { faSquarePlus } from '@fortawesome/free-solid-svg-icons' 13 | 14 | class CreateRulesetContainer extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = {name: '', error: {}, fileExist: false, message: {}}; 19 | this.onChangeName = this.onChangeName.bind(this); 20 | this.handleAdd = this.handleAdd.bind(this); 21 | } 22 | 23 | onChangeName(e) { 24 | this.setState({name:e.target.value}); 25 | } 26 | 27 | handleAdd(e){ 28 | e.preventDefault(); 29 | const history = createHashHistory(); 30 | if (!this.state.name || !this.state.name.trim()) { 31 | this.setState({error: {name: 'Please specify value'}}); 32 | } else if (includes(this.props.rulesetnames, this.state.name)) { 33 | this.setState({ fileExist: true, message: RULE_AVAILABLE_CREATE }); 34 | } else { 35 | this.props.addRuleset(this.state.name); 36 | history.push('./ruleset'); 37 | } 38 | 39 | } 40 | 41 | render() { 42 | const { fileExist, message } = this.state; 43 | 44 | return ( 45 | <div className="single-panel-container"> 46 | { fileExist && <Notification body={message.body} heading={message.heading} type={message.type} /> } 47 | <TitlePanel title="Create Rules" titleClass={faSquarePlus}> 48 | <form> 49 | <div className="upload-panel"> 50 | <InputField label="Name" onChange={this.onChangeName} value={this.state.name} error={this.state.error.name} /> 51 | <Button label={'Create'} onConfirm={this.handleAdd} classname="primary-btn" type="submit" /> 52 | </div> 53 | </form> 54 | </TitlePanel> 55 | </div> 56 | ) 57 | } 58 | 59 | } 60 | 61 | const mapStateToProps = state => ({ 62 | rulesetnames: state.ruleset.rulesets.map(r => r.name), 63 | }) 64 | 65 | const mapDispatchToProps = dispatch => ({ 66 | addRuleset: (name) => dispatch(addRuleset(name)) 67 | }); 68 | 69 | CreateRulesetContainer.defaultProps = { 70 | addRuleset: () => false, 71 | rulesetnames: [], 72 | }; 73 | 74 | CreateRulesetContainer.propTypes = { 75 | rulesetnames: PropTypes.array, 76 | addRuleset: PropTypes.func, 77 | } 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(CreateRulesetContainer); -------------------------------------------------------------------------------- /src/containers/ruleset/ruleset-container.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable no-undef */ 3 | import React, { Component } from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import PageTitle from '../../components/title/page-title'; 7 | import Tabs from '../../components/tabs/tabs'; 8 | import Attributes from '../../components/attributes/attributes'; 9 | import Decisions from '../../components/decisions/decision'; 10 | import ValidateRules from '../../components/validate/validate-rules'; 11 | import { handleAttribute } from '../../actions/attributes'; 12 | import { handleDecision } from '../../actions/decisions'; 13 | import Banner from '../../components/panel/banner'; 14 | import * as Message from '../../constants/messages'; 15 | import { groupBy } from 'lodash/collection'; 16 | import RuleErrorBoundary from '../../components/error/ruleset-error'; 17 | import SweetAlert from 'react-bootstrap-sweetalert'; 18 | 19 | const tabs = [{name: 'Facts'}, {name: 'Decisions'}, {name: 'Validate'}, {name: 'Generate'}]; 20 | class RulesetContainer extends Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | this.state = {activeTab: 'Facts', generateFlag: false }; 25 | this.generateFile = this.generateFile.bind(this); 26 | this.cancelAlert = this.cancelAlert.bind(this); 27 | } 28 | 29 | handleTab = (tabName) => { 30 | this.setState({activeTab: tabName}); 31 | } 32 | 33 | generateFile() { 34 | const { ruleset } = this.props; 35 | const fileData = JSON.stringify(ruleset, null,'\t'); 36 | const blob = new Blob([fileData], { type: "application/json" }); 37 | const url = URL.createObjectURL(blob); 38 | const link = document.createElement('a'); 39 | link.download = ruleset.name +'.json'; 40 | link.href = url; 41 | link.click(); 42 | this.setState({ generateFlag: true }); 43 | } 44 | 45 | cancelAlert() { 46 | this.setState({ generateFlag: false }) 47 | } 48 | 49 | successAlert = () => { 50 | const { name } = this.props.ruleset; 51 | return (<SweetAlert 52 | success 53 | title={"File generated!"} 54 | onConfirm={this.cancelAlert} 55 | > {`${name} rule is succefully generated at your default download location`} 56 | </SweetAlert>); 57 | } 58 | 59 | render() { 60 | const { attributes, decisions, name } = this.props.ruleset; 61 | 62 | const indexedDecisions = decisions && decisions.length > 0 && 63 | decisions.map((decision, index) => ({ ...decision, index })); 64 | 65 | let outcomes; 66 | if (indexedDecisions && indexedDecisions.length > 0) { 67 | outcomes = groupBy(indexedDecisions, data => data.event.type); 68 | } 69 | 70 | const message = this.props.updatedFlag ? Message.MODIFIED_MSG : Message.NO_CHANGES_MSG; 71 | 72 | return <div> 73 | <RuleErrorBoundary> 74 | <PageTitle name={name} /> 75 | <Tabs tabs={tabs} onConfirm={this.handleTab} activeTab={this.state.activeTab} /> 76 | <div className="tab-page-container"> 77 | {this.state.activeTab === 'Facts' && <Attributes attributes={attributes} 78 | handleAttribute={this.props.handleAttribute }/>} 79 | {this.state.activeTab === 'Decisions' && <Decisions decisions={indexedDecisions || []} attributes={attributes} 80 | handleDecisions={this.props.handleDecisions} outcomes={outcomes}/>} 81 | {this.state.activeTab === 'Validate' && <ValidateRules attributes={attributes} decisions={decisions} />} 82 | {this.state.activeTab === 'Generate' && <Banner message={message} ruleset={this.props.ruleset} onConfirm={this.generateFile}/> } 83 | {this.state.generateFlag && this.successAlert()} 84 | </div> 85 | </RuleErrorBoundary> 86 | </div> 87 | } 88 | } 89 | 90 | RulesetContainer.propTypes = { 91 | ruleset: PropTypes.object, 92 | handleAttribute: PropTypes.func, 93 | handleDecisions: PropTypes.func, 94 | updatedFlag: PropTypes.bool, 95 | runRules: PropTypes.func, 96 | } 97 | 98 | RulesetContainer.defaultProps = { 99 | ruleset: {}, 100 | handleAttribute: () => false, 101 | handleDecisions: () => false, 102 | updatedFlag: false, 103 | } 104 | 105 | const mapStateToProps = (state) => ({ 106 | ruleset: state.ruleset.rulesets[state.ruleset.activeRuleset], 107 | updatedFlag: state.ruleset.updatedFlag, 108 | }); 109 | 110 | const mapDispatchToProps = (dispatch) => ({ 111 | handleAttribute: (operation, attribute, index) => dispatch(handleAttribute(operation, attribute, index)), 112 | handleDecisions: (operation, decision) => dispatch(handleDecision(operation, decision)), 113 | }); 114 | 115 | export default connect(mapStateToProps, mapDispatchToProps)(RulesetContainer); -------------------------------------------------------------------------------- /src/context/apperance-context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const theme = { 4 | background: 'light', 5 | toggleBackground: () => {}, 6 | }; 7 | 8 | const ApperanceContext = React.createContext(theme); 9 | 10 | export default ApperanceContext; -------------------------------------------------------------------------------- /src/data-objects/footer-links.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { 4 | "label": "v1.2.0" 5 | }, 6 | { 7 | "label": "Report issue", 8 | "src": "https://github.com/vinzdeveloper/json-rule-editor/issues" 9 | }, 10 | { 11 | "label": "Docs", 12 | "src": "https://vinzdeveloper.github.io/json-rule-editor/docs/" 13 | } 14 | ] -------------------------------------------------------------------------------- /src/data-objects/operator.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": ["equal", "notEqual"], 3 | "number": ["equal", "lessThan", "lessThanInclusive", "greaterThan", "greaterThanInclusive", "notEqual"], 4 | "array": ["contains", "doesNotContain", "in", "notIn"], 5 | "object": ["equal", "notEqual", "lessThan", "lessThanInclusive", "greaterThan", "greaterThanInclusive", "contains", "doesNotContain", "in", "notIn"] 6 | } -------------------------------------------------------------------------------- /src/reducers/app-reducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_NAV_STATE, LOG_IN} from '../actions/action-types'; 2 | 3 | const initialState = { 4 | navState: 'closed', 5 | loggedIn: false, 6 | } 7 | 8 | const AppReducer = (state=initialState, action) => { 9 | const type = action.type; 10 | switch(type) { 11 | case UPDATE_NAV_STATE: { 12 | let nav = 'closed'; 13 | if (action.payload && action.payload.flag === 'open') { 14 | nav = 'open'; 15 | } 16 | return { ...state, navState: nav }; 17 | } 18 | case LOG_IN: 19 | return { ...state, loggedIn: true }; 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export default AppReducer; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import AppReducer from './app-reducer'; 3 | import RulesetReducer from './ruleset-reducer'; 4 | 5 | const reducers = combineReducers({ 6 | app: AppReducer, 7 | ruleset: RulesetReducer, 8 | }); 9 | 10 | export default reducers; -------------------------------------------------------------------------------- /src/reducers/ruleset-reducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions/action-types'; 2 | import { cloneDeep } from 'lodash/lang'; 3 | import { findIndex } from 'lodash/array'; 4 | 5 | const initialState = { 6 | rulesets: [], 7 | activeRuleset: 0, 8 | updatedFlag: false, 9 | uploadedRules: [], 10 | } 11 | 12 | 13 | const replaceRulesetByIndex = (rulesets, targetset, index) => { 14 | return [ ...rulesets.slice(0, index), targetset, ...rulesets.slice(index + 1)]; 15 | } 16 | 17 | 18 | function ruleset(state = initialState, action='') { 19 | 20 | switch(action.type) { 21 | 22 | case ActionTypes.UPLOAD_RULESET: { 23 | 24 | const { ruleset } = action.payload; 25 | const rulesets = state.rulesets.concat(ruleset); 26 | return { ...state, rulesets: cloneDeep(rulesets), uploadedRules: cloneDeep(rulesets)} 27 | } 28 | 29 | case ActionTypes.ADD_RULESET: { 30 | 31 | const { name } = action.payload; 32 | const rulset = { name, attributes: [], decisions: []}; 33 | const count = state.rulesets.length === 0 ? 0 : state.rulesets.length; 34 | return { ...state, rulesets: state.rulesets.concat(rulset), activeRuleset: count} 35 | } 36 | 37 | case ActionTypes.UPDATE_RULESET_INDEX: { 38 | 39 | const { name } = action.payload; 40 | const index = findIndex(state.rulesets, { name }); 41 | return { ...state, activeRuleset: index} 42 | } 43 | 44 | case ActionTypes.ADD_DECISION: { 45 | 46 | const { condition } = action.payload; 47 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 48 | activeRuleSet.decisions = activeRuleSet.decisions.concat(condition); 49 | 50 | return { ...state, 51 | updatedFlag: true, 52 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 53 | } 54 | 55 | case ActionTypes.UPDATE_DECISION: { 56 | const { condition, decisionIndex } = action.payload; 57 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 58 | 59 | activeRuleSet.decisions[decisionIndex] = condition; 60 | 61 | return { ...state, 62 | updatedFlag: true, 63 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 64 | } 65 | case ActionTypes.REMOVE_DECISION: { 66 | 67 | const { decisionIndex } = action.payload; 68 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 69 | 70 | activeRuleSet.decisions.splice(decisionIndex, 1); 71 | 72 | return { ...state, 73 | updatedFlag: true, 74 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 75 | } 76 | 77 | case ActionTypes.REMOVE_DECISIONS: { 78 | 79 | const { outcome } = action.payload; 80 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 81 | 82 | activeRuleSet.decisions = activeRuleSet.decisions.filter(decision => decision.event && decision.event.type !== outcome); 83 | 84 | return { ...state, 85 | updatedFlag: true, 86 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 87 | } 88 | 89 | 90 | case ActionTypes.ADD_ATTRIBUTE: { 91 | const { attribute } = action.payload; 92 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 93 | activeRuleSet.attributes.push(attribute); 94 | 95 | return { ...state, 96 | updatedFlag: true, 97 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 98 | } 99 | 100 | case ActionTypes.UPDATE_ATTRIBUTE: { 101 | const { attribute, index } = action.payload; 102 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 103 | activeRuleSet.attributes.splice(index, 1, attribute); 104 | 105 | return { ...state, 106 | updatedFlag: true, 107 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 108 | } 109 | 110 | case ActionTypes.REMOVE_ATTRIBUTE: { 111 | 112 | const { index } = action.payload; 113 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 114 | activeRuleSet.attributes.splice(index, 1); 115 | 116 | return { ...state, 117 | updatedFlag: true, 118 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 119 | } 120 | 121 | case ActionTypes.RESET_ATTRIBUTE: { 122 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 123 | if(state.uploadedRules[state.activeRuleset] && state.uploadedRules[state.activeRuleset].attributes) { 124 | activeRuleSet.attributes = cloneDeep(state.uploadedRules[state.activeRuleset].attributes); 125 | 126 | return { ...state, 127 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 128 | } 129 | return { ...state }; 130 | } 131 | 132 | case ActionTypes.RESET_DECISION: { 133 | const activeRuleSet = { ...state.rulesets[state.activeRuleset] }; 134 | if(state.uploadedRules[state.activeRuleset] && state.uploadedRules[state.activeRuleset].decisions) { 135 | activeRuleSet.decisions = cloneDeep(state.uploadedRules[state.activeRuleset].decisions); 136 | 137 | return { ...state, 138 | rulesets: replaceRulesetByIndex(state.rulesets, activeRuleSet, state.activeRuleset)} 139 | } 140 | return { ...state }; 141 | } 142 | 143 | 144 | default: 145 | return { ...state }; 146 | } 147 | } 148 | 149 | export default ruleset; -------------------------------------------------------------------------------- /src/routes/app-routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {HashRouter, Switch, Route } from 'react-router-dom'; 3 | import HomeContainer from '../containers/home/home-container'; 4 | import RulesetContainer from '../containers/ruleset/ruleset-container'; 5 | import CreateRulesetContainer from '../containers/ruleset/create-ruleset-container'; 6 | import AppearanceContainer from '../containers/app/appearance-container'; 7 | import PropTypes from 'prop-types'; 8 | 9 | const AppRoutes = (props) => { 10 | const { background } = props.appctx; 11 | return (<div className={`main-container ${props.closedState ? 'closed': 'open'} ${background}`}> 12 | <HashRouter> 13 | <Switch> 14 | <Route path="/home" exact component={HomeContainer} /> 15 | <Route path="/ruleset" exact component={RulesetContainer} /> 16 | <Route path="/create-ruleset" exact component={CreateRulesetContainer} /> 17 | <Route path="/appearance" exact component={AppearanceContainer} /> 18 | </Switch> 19 | </HashRouter> 20 | </div>); 21 | 22 | }; 23 | 24 | AppRoutes.defaultProps = { 25 | closedState: false, 26 | loggedIn: false, 27 | appctx: {}, 28 | }; 29 | 30 | AppRoutes.propTypes = { 31 | closedState: PropTypes.bool, 32 | loggedIn: PropTypes.bool, 33 | appctx: PropTypes.object, 34 | } 35 | 36 | export default AppRoutes; -------------------------------------------------------------------------------- /src/sass/_utils.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin link { 3 | background: $btn-bg-clr; 4 | border: 1px solid $btn-bg-clr; 5 | border-radius: 10px; 6 | font-family: $btn-font-family; 7 | color: $btn-color; 8 | cursor: pointer; 9 | } -------------------------------------------------------------------------------- /src/sass/base.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './utils.scss'; 3 | @import './components/index.scss'; 4 | @import './theme/index.scss'; 5 | 6 | h1 { 7 | color: blue; 8 | } 9 | 10 | .main-container { 11 | font-family: $title-font-family; 12 | margin: { 13 | right: 30px; 14 | } 15 | padding: { 16 | top: 10px; 17 | bottom: 30px; 18 | }; 19 | position: relative; 20 | overflow-x: hidden; 21 | 22 | &.closed { 23 | left: 100px; 24 | width: calc(100% - 140px); 25 | } 26 | 27 | &.open { 28 | left: $sideNavWidth + 40px; 29 | width: calc(100% - 360px); 30 | } 31 | } 32 | 33 | label { 34 | color: $form-label-color; 35 | font-size: 18px; 36 | margin-right: 20px; 37 | } 38 | 39 | input[type="file"] { 40 | display: none; 41 | } 42 | 43 | input[type="radio"] { 44 | margin-right: 10px; 45 | } 46 | 47 | body { 48 | margin: 0px; 49 | } 50 | 51 | hr { 52 | margin: 20px 0px 10px; 53 | border: 1px dashed #aaa; 54 | } 55 | 56 | a { 57 | text-decoration: none; 58 | } 59 | -------------------------------------------------------------------------------- /src/sass/components/apperance.scss: -------------------------------------------------------------------------------- 1 | .theme-container { 2 | display: flex; 3 | width: 100%; 4 | 5 | .theme-icon { 6 | margin-left: 30px; 7 | text-align: center; 8 | } 9 | 10 | 11 | span { 12 | 13 | &.light-mode { 14 | background: url('../../assets/icons/light-mode.png') no-repeat; 15 | background-size: contain; 16 | display: block; 17 | padding: 20px 40px; 18 | } 19 | 20 | &.dark-mode { 21 | background: url('../../assets/icons/dark-mode.png') no-repeat; 22 | background-size: contain; 23 | display: block; 24 | padding: 20px 40px; 25 | } 26 | 27 | &.mdblue-mode { 28 | background: url('../../assets/icons/mdblue-mode.png') no-repeat; 29 | background-size: contain; 30 | display: block; 31 | padding: 20px 40px; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/sass/components/attributes.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | 3 | 4 | 5 | .attributes-header { 6 | color: $form-label-color; 7 | display: flex; 8 | flex-wrap: wrap; 9 | font-family: $tab-font-family; 10 | justify-content: flex-end; 11 | margin: 0; 12 | width: 100%; 13 | 14 | &.dark { 15 | color: $link-clr-drk; 16 | 17 | .plus-icon { 18 | background: url('../../assets/icons/plus-rounded-dark.svg') no-repeat; 19 | background-size: contain; 20 | } 21 | } 22 | 23 | &.md-blue { 24 | color: $link-clr-drk; 25 | 26 | .plus-icon { 27 | background: url('../../assets/icons/plus-rounded-dark.svg') no-repeat; 28 | background-size: contain; 29 | } 30 | } 31 | 32 | &.light { 33 | color: $form-label-color; 34 | 35 | .plus-icon { 36 | background: url('../../assets/icons/plus-icon-rounded.svg') no-repeat; 37 | background-size: contain; 38 | } 39 | } 40 | 41 | .attr-link { 42 | cursor: pointer; 43 | margin: { 44 | left: 25px; 45 | right: 25px; 46 | top: 10px; 47 | } 48 | 49 | .text { 50 | margin-left: 30px; 51 | } 52 | } 53 | 54 | .attr-link:hover { 55 | text-decoration: underline; 56 | } 57 | 58 | .plus-icon { 59 | background: url('../../assets/icons/plus-icon-rounded.svg') no-repeat; 60 | background-size: contain; 61 | margin: 0 0px 0 0; 62 | padding: 0; 63 | padding-bottom: 35px; 64 | position: absolute; 65 | width: 35px; 66 | } 67 | 68 | .submit-icon { 69 | background: url('../../assets/icons/submit-icon-green-rounded.svg') no-repeat; 70 | background-size: contain; 71 | margin: 0 0px 0 0; 72 | padding: 0; 73 | padding-bottom: 25px; 74 | position: absolute; 75 | width: 25px; 76 | } 77 | 78 | .reset-icon { 79 | background: url('../../assets/icons/reset-icon-red-rounded.svg') no-repeat; 80 | background-size: contain; 81 | margin: 0 0px 0 0; 82 | padding: 0; 83 | padding-bottom: 25px; 84 | position: absolute; 85 | width: 25px; 86 | } 87 | 88 | .btn-container { 89 | margin-top: 0px; 90 | } 91 | } 92 | 93 | td.attributes-header { 94 | justify-content: flex-start; 95 | } 96 | 97 | @mixin attr-linktype($color) { 98 | background-color: $color; 99 | border-radius: 5px; 100 | color: $btn-color; 101 | padding: 2px 5px; 102 | font-size: 12px; 103 | text-transform: uppercase; 104 | } 105 | 106 | .string { 107 | @include attr-linktype(map-get($attribute-type, "string")); 108 | } 109 | 110 | .boolean { 111 | @include attr-linktype(map-get($attribute-type, "boolean")); 112 | } 113 | 114 | .array { 115 | @include attr-linktype(map-get($attribute-type, "array")); 116 | 117 | } 118 | 119 | .object { 120 | @include attr-linktype(map-get($attribute-type, "object")); 121 | } 122 | 123 | .number { 124 | @include attr-linktype(map-get($attribute-type, "number")); 125 | } 126 | 127 | .view-attribute { 128 | border-bottom: 2px solid #ddd; 129 | display: flex; 130 | flex-direction: row; 131 | justify-content: space-between; 132 | padding: 10px; 133 | } 134 | 135 | .md-blue { 136 | .view-attribute { 137 | color: $form-label-color-drk; 138 | } 139 | } 140 | 141 | .dark { 142 | color: $form-label-color-drk; 143 | } 144 | 145 | .view-params-container { 146 | width: 40%; 147 | color: $dark-color; 148 | } 149 | -------------------------------------------------------------------------------- /src/sass/components/button.scss: -------------------------------------------------------------------------------- 1 | 2 | .nav-btn { 3 | margin: 30px 0; 4 | } 5 | 6 | .nav-glass-btn { 7 | @include link; 8 | font-size: $btn-font-size; 9 | height: 50px; 10 | width: 300px; 11 | 12 | &:hover { 13 | background: $btn-bg-clr-hover; 14 | } 15 | } 16 | 17 | .btn-container { 18 | display: flex; 19 | margin: { 20 | right: 20px; 21 | top: 20px; 22 | } 23 | } 24 | 25 | .upload-panel { 26 | .btn-container { 27 | margin-top: 30px; 28 | } 29 | } 30 | 31 | .btn-group { 32 | display: flex; 33 | margin-top: 10px; 34 | } 35 | 36 | .btn { 37 | border-radius: 5px; 38 | color: $btn-color; 39 | font-size: 16px; 40 | font-family: $form-font-family; 41 | height: 45px; 42 | outline:none; 43 | padding-top: 0; 44 | width: 140px; 45 | } 46 | 47 | a.btn.btn-lg.btn-primary { 48 | border: 0; 49 | } 50 | 51 | .btn:hover { 52 | cursor: pointer; 53 | } 54 | 55 | .btn:focus { 56 | cursor: pointer; 57 | } 58 | 59 | .primary-btn { 60 | background-color: $primary-btn-color; 61 | border: 1px solid $primary-btn-color; 62 | } 63 | 64 | .primary-btn:focus { 65 | background-color: #0f7965; 66 | border: 1px solid #0f7965; 67 | } 68 | 69 | .cancel-btn { 70 | background-color: $cancel-btn-color; 71 | border: 1px solid $cancel-btn-color; 72 | } 73 | 74 | .cancel-btn:focus { 75 | background-color: #405367; 76 | border: 1px solid #405367; 77 | } 78 | 79 | .btn-success { 80 | background-color: $primary-btn-color; 81 | border: 1px solid $primary-btn-color; 82 | line-height: 45px; 83 | text-decoration: none; 84 | } 85 | 86 | .btn-link { 87 | background-color: $cancel-btn-color; 88 | border: 1px solid $cancel-btn-color; 89 | line-height: 45px; 90 | text-decoration: none; 91 | } 92 | 93 | .btn-danger { 94 | background-color: $danger; 95 | border: 1px solid $danger; 96 | line-height: 45px; 97 | text-decoration: none; 98 | } 99 | 100 | .btn-primary { 101 | background-color: $primary-btn-color; 102 | border: 1px solid $primary-btn-color; 103 | line-height: 45px; 104 | text-decoration: none; 105 | } 106 | 107 | .btn-dark { 108 | background-color: $button-dark; 109 | border: 1px solid $button-dark; 110 | } 111 | 112 | .btn-warning { 113 | background-color: $warning-btn; 114 | border: 1px solid $warning-btn; 115 | } 116 | 117 | .btn-warning:focus { 118 | background-color: $danger; 119 | } 120 | 121 | .btn-toolbar { 122 | background-color: $toobar-btn-color; 123 | border: 1px solid $toobar-btn-color; 124 | width: 100px; 125 | height: 35px; 126 | } 127 | 128 | .btn-toolbar:focus { 129 | background-color: $form-field-color; 130 | } 131 | 132 | .btn-group-container { 133 | display: flex; 134 | flex-direction: row; 135 | 136 | .btn-grp { 137 | border: 1px solid $form-field-color; 138 | cursor: pointer; 139 | } 140 | 141 | .btn-grp:first-child { 142 | border-top-left-radius: 5px; 143 | border-bottom-left-radius: 5px; 144 | } 145 | 146 | .btn-grp:last-child { 147 | border-top-right-radius: 5px; 148 | border-bottom-right-radius: 5px; 149 | } 150 | 151 | button { 152 | border: none; 153 | border-radius: 5px; 154 | background-color: #fff; 155 | cursor: pointer; 156 | font-family: $form-font-family; 157 | font-size: 13px; 158 | height: 35px; 159 | margin: 0; 160 | outline:none; 161 | padding: 10px; 162 | } 163 | 164 | &.dark { 165 | 166 | .btn-grp { 167 | border: 1px solid $panel-bg-color; 168 | } 169 | button { 170 | border-radius: 0; 171 | } 172 | button:hover { 173 | background-color: $primary-btn-color; 174 | border-radius: 0; 175 | color: #fff; 176 | } 177 | button:focus { 178 | background-color: $primary-btn-color; 179 | border-radius: 0; 180 | color: #fff; 181 | } 182 | button.active { 183 | background-color: $primary-btn-color; 184 | border-radius: 0; 185 | color: #fff; 186 | } 187 | } 188 | 189 | &.md-blue { 190 | 191 | .btn-grp { 192 | border: 1px solid $panel-bg-color; 193 | } 194 | button { 195 | border-radius: 0; 196 | } 197 | button:hover { 198 | background-color: $primary-btn-color; 199 | border-radius: 0; 200 | color: #fff; 201 | } 202 | button:focus { 203 | background-color: $primary-btn-color; 204 | border-radius: 0; 205 | color: #fff; 206 | } 207 | button.active { 208 | background-color: $primary-btn-color; 209 | border-radius: 0; 210 | color: #fff; 211 | } 212 | } 213 | 214 | &.light { 215 | 216 | .btn-grp { 217 | border: 1px solid $form-field-color; 218 | } 219 | button { 220 | border-radius: 5px; 221 | } 222 | button:hover { 223 | background-color: $form-field-color; 224 | border-radius: 0; 225 | color: #fff; 226 | } 227 | button:focus { 228 | background-color: $form-field-color; 229 | border-radius: 0; 230 | color: #fff; 231 | } 232 | button.active { 233 | background-color: $form-field-color; 234 | border-radius: 0; 235 | color: #fff; 236 | } 237 | 238 | } 239 | } -------------------------------------------------------------------------------- /src/sass/components/decisions.scss: -------------------------------------------------------------------------------- 1 | .rule-flex-container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-top: 50px; 5 | margin-left: 50px; 6 | 7 | .decision-box { 8 | border-bottom: 2px solid #ccc; 9 | margin-bottom: 30px; 10 | padding-bottom: 20px; 11 | width: 100%; 12 | } 13 | 14 | .tool-flex { 15 | display: flex; 16 | justify-content: flex-end; 17 | width: 100%; 18 | 19 | div { 20 | width: 30px; 21 | } 22 | } 23 | } 24 | 25 | .add-rulecase-wrapper { 26 | border-bottom: 2px dashed $form-label-color; 27 | border-top: 2px dashed $form-label-color; 28 | margin-top: 20px; 29 | padding: 20px 0; 30 | } 31 | 32 | .add-decision-step { 33 | display: flex; 34 | color: $form-label-color; 35 | font-family: $form-font-family; 36 | flex-wrap: wrap; 37 | 38 | .step1 { 39 | margin-right: 10%; 40 | } 41 | 42 | .step2 { 43 | margin-right: 10%; 44 | } 45 | 46 | div { 47 | margin-bottom: 10px; 48 | } 49 | } 50 | 51 | .dark { 52 | .add-decision-step { 53 | color: $form-label-color-drk; 54 | } 55 | } 56 | 57 | .md-blue { 58 | .add-decision-step { 59 | color: $form-label-color-drk; 60 | } 61 | } 62 | 63 | .light { 64 | .add-decision-step { 65 | color: $form-label-color; 66 | } 67 | } 68 | 69 | .add-field-panel { 70 | display: flex; 71 | flex-direction: row; 72 | font-family: $form-font-family; 73 | justify-content: space-between; 74 | margin-top: 20px; 75 | 76 | div { 77 | flex-grow: 1; 78 | width: 100%; 79 | } 80 | 81 | .btn-container { 82 | margin-top: 27px; 83 | } 84 | 85 | &.half-width { 86 | width: 50%; 87 | } 88 | } -------------------------------------------------------------------------------- /src/sass/components/forms.scss: -------------------------------------------------------------------------------- 1 | .form-groups-inline { 2 | font-family: $form-font-family; 3 | display: flex; 4 | flex-wrap: wrap; 5 | 6 | div { 7 | margin-top: 10px; 8 | } 9 | 10 | .form-field { 11 | width: 35%; 12 | } 13 | } 14 | 15 | .upload-panel { 16 | .form-field { 17 | width: 70%; 18 | } 19 | } 20 | 21 | .form-field { 22 | margin-right: 10%; 23 | margin-top: 5px; 24 | 25 | input { 26 | border: 1px solid $form-border-color; 27 | border-radius: 4px; 28 | color: $form-field-color; 29 | font-size: 16px; 30 | height: 35px; 31 | margin-top: 5px; 32 | padding-left: 10px; 33 | width: 90%; 34 | } 35 | 36 | select { 37 | background-color: #fff; 38 | border: 1px solid $form-border-color; 39 | border-radius: 4px; 40 | font-size: 16px; 41 | color: $form-field-color; 42 | height: 40px; 43 | margin-top: 5px; 44 | padding-left: 10px; 45 | width: 90%; 46 | } 47 | 48 | .error { 49 | border-color: red; 50 | } 51 | 52 | .readOnly { 53 | background-color: #eee; 54 | border: 1px dashed; 55 | } 56 | } 57 | 58 | .form-error { 59 | color: red; 60 | font-family: $form-font-family; 61 | font-size: 16px; 62 | } -------------------------------------------------------------------------------- /src/sass/components/home.scss: -------------------------------------------------------------------------------- 1 | .page-container { 2 | padding: 60px 20px; 3 | } 4 | 5 | .single-panel-container { 6 | display: flex; 7 | justify-content: center; 8 | margin-top: 150px; 9 | 10 | .upload-panel{ 11 | display: flex; 12 | margin-top: 50px; 13 | } 14 | 15 | .upload-section { 16 | border-right: 1px solid #aaa; 17 | padding-top: 25px; 18 | width: 50%; 19 | margin-right:20px; 20 | } 21 | 22 | .drop-section { 23 | border: 1px dashed #aaa; 24 | border-radius: 5px; 25 | color: #73879C; 26 | font-size: 18px; 27 | height: 70px; 28 | line-height: 30px; 29 | margin-left: 20px; 30 | padding-top: 35px; 31 | text-align: center; 32 | width: 100%; 33 | 34 | label { 35 | color: $link-color; 36 | margin-right: 0px; 37 | text-decoration: underline; 38 | } 39 | 40 | label:hover { 41 | cursor: pointer; 42 | text-decoration: underline; 43 | } 44 | 45 | &.dark { 46 | color: #bca8f1; 47 | 48 | label { 49 | color: $link-clr-drk; 50 | } 51 | } 52 | 53 | &.md-blue { 54 | color: #bca8f1; 55 | 56 | label { 57 | color: $link-clr-drk; 58 | } 59 | } 60 | 61 | &.light { 62 | color: #73879C; 63 | 64 | label { 65 | color: $link-color; 66 | } 67 | } 68 | } 69 | 70 | .file-drop-msg { 71 | font-size: 14px; 72 | } 73 | 74 | .btn-group { 75 | justify-content: center; 76 | margin-top: 25px; 77 | } 78 | 79 | 80 | } 81 | 82 | .browse-label { 83 | background-color: $cancel-btn-color; 84 | border-radius: 5px; 85 | color: $btn-color; 86 | font-size: 16px; 87 | font-family: $form-font-family; 88 | height: 40px; 89 | line-height: 40px; 90 | margin-top: 20px; 91 | outline:none; 92 | padding-top: 0; 93 | text-align: center; 94 | width: 140px; 95 | } 96 | 97 | .home-container { 98 | display: flex; 99 | flex-direction: column; 100 | height: 90vh; 101 | justify-content: space-between; 102 | } 103 | 104 | .footer-container { 105 | display: flex; 106 | 107 | &.home-page { 108 | flex-direction: row-reverse; 109 | 110 | a { 111 | font-size: 14px; 112 | } 113 | } 114 | 115 | &.sidenav { 116 | flex-direction: column-reverse; 117 | 118 | a { 119 | color: $btn-color; 120 | font-size: 14px; 121 | } 122 | } 123 | 124 | div { 125 | margin-left: 20px; 126 | margin-bottom: 20px; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/sass/components/index.scss: -------------------------------------------------------------------------------- 1 | @import './title.scss'; 2 | @import './nav.scss'; 3 | @import './button.scss'; 4 | @import './home.scss'; 5 | @import './tabs.scss'; 6 | @import './attributes.scss'; 7 | @import './search.scss'; 8 | @import './panel.scss'; 9 | @import './forms.scss'; 10 | @import './table.scss'; 11 | @import './tree.scss'; 12 | @import './decisions.scss'; 13 | @import './apperance.scss'; -------------------------------------------------------------------------------- /src/sass/components/nav.scss: -------------------------------------------------------------------------------- 1 | .nav-container { 2 | background-color: $title-bg-clr; 3 | font-family: $title-font-family; 4 | height: 100%; 5 | left: 0; 6 | margin-top: -70px; 7 | overflow-y: auto; 8 | position: fixed; 9 | padding: 0 10px 25px; 10 | 11 | &.closed { 12 | width: 40px; 13 | } 14 | 15 | &.open { 16 | width: $sideNavWidth; 17 | } 18 | 19 | &.light { 20 | background-color: $title-bg-clr; 21 | } 22 | 23 | &.dark { 24 | background-color: $title-bg-clr-drk; 25 | } 26 | 27 | &.md-blue { 28 | background-color: $title-bg-clr-mdblue; 29 | } 30 | } 31 | 32 | .menu-bar { 33 | cursor: pointer; 34 | font-size: 19px; 35 | margin-top: 30px; 36 | margin-bottom: 25px; 37 | 38 | a { 39 | color: $title-color; 40 | } 41 | } 42 | 43 | 44 | .links-section { 45 | display: flex; 46 | height: 90vh; 47 | flex-direction: column; 48 | justify-content: space-between; 49 | } 50 | 51 | .link-container { 52 | list-style-type: none; 53 | margin-top: 5px; 54 | padding: 0; 55 | 56 | li { 57 | color: $btn-color; 58 | font-family: $btn-font-family; 59 | font-size: 15px; 60 | height: 40px; 61 | line-height: 40px; 62 | padding: 5px 10px; 63 | } 64 | 65 | li:hover { 66 | background: $btn-bg-clr; 67 | } 68 | 69 | a { 70 | color: $btn-color; 71 | } 72 | 73 | .link-heading { 74 | font-size: 20px; 75 | 76 | a { 77 | color: $btn-color; 78 | 79 | .rules-icon { 80 | background: url('../../assets/icons/rules.svg') no-repeat; 81 | background-size: contain; 82 | margin: 0 20px 0 0; 83 | padding: 0; 84 | padding-bottom: 15%; 85 | position: absolute; 86 | width: 100%; 87 | } 88 | 89 | span.text { 90 | margin-left: 75px; 91 | } 92 | 93 | } 94 | 95 | a::after { 96 | content: '>'; 97 | float: right; 98 | transition: transform 0.5s; 99 | } 100 | 101 | a.active::after { 102 | transform: rotate(90deg); 103 | transition: transform 0.5s; 104 | } 105 | } 106 | 107 | .navmenu { 108 | cursor: pointer; 109 | padding-left: 30px; 110 | 111 | a { 112 | color: $btn-color; 113 | font-size: 16px; 114 | text-decoration: none; 115 | } 116 | 117 | .icon { 118 | font-size: 19px; 119 | } 120 | 121 | span.text { 122 | margin-left: 40px; 123 | } 124 | } 125 | 126 | .sublink-container { 127 | display: none; 128 | list-style-type: none; 129 | top: 10px; 130 | 131 | &.visible { 132 | display: block; 133 | } 134 | 135 | li { 136 | border-left: 3px solid $border-left-color; 137 | cursor: pointer; 138 | line-height: 20px; 139 | visibility: hidden; 140 | height: auto; 141 | 142 | &.visible { 143 | visibility: visible; 144 | padding: 10px; 145 | } 146 | 147 | &.active a{ 148 | color: $link-active-color; 149 | } 150 | } 151 | 152 | a { 153 | span.text { 154 | margin-left: 10px; 155 | } 156 | } 157 | 158 | a::after { 159 | content: ''; 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/sass/components/panel.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | 3 | .panel-wrapper { 4 | border: 1px solid #006a4d; 5 | border-radius: 10px; 6 | margin-top: 20px; 7 | padding: 20px; 8 | } 9 | 10 | .panel-box-wrapper { 11 | border: 1px solid #bbb; 12 | box-shadow: 1px 3px #eee; 13 | display: flex; 14 | font-family: $tab-font-family; 15 | font-size: 17px; 16 | flex-wrap: wrap; 17 | margin-top: 30px; 18 | padding: 20px; 19 | 20 | &.string-type { 21 | border-left: 5px solid map-get($attribute-type, "string"); 22 | } 23 | 24 | &.number-type { 25 | border-left: 5px solid map-get($attribute-type, "number"); 26 | } 27 | 28 | &.date-type { 29 | border-left: 5px solid map-get($attribute-type, "date"); 30 | } 31 | 32 | &.object-type { 33 | border-left: 5px solid map-get($attribute-type, "object"); 34 | } 35 | 36 | &.boolean-type { 37 | border-left: 5px solid map-get($attribute-type, "boolean"); 38 | } 39 | 40 | &.array-type { 41 | border-left: 5px solid map-get($attribute-type, "array"); 42 | } 43 | 44 | div { 45 | margin: 0 20px; 46 | width: 10%; 47 | } 48 | 49 | .index { 50 | width: 1%; 51 | } 52 | 53 | .name { 54 | width: 25%; 55 | } 56 | 57 | .type { 58 | width: 15%; 59 | } 60 | 61 | .type-badge { 62 | background-color: map-get($attribute-type, "boolean"); 63 | border-radius: 5px; 64 | color: white; 65 | font-size: 14px; 66 | padding: 4px 8px; 67 | 68 | } 69 | 70 | .menu { 71 | margin-left: 20px; 72 | width: auto; 73 | } 74 | 75 | a { 76 | color: $link-color; 77 | cursor: pointer; 78 | font-family: $tab-font-family; 79 | font-size: 14px; 80 | margin-right: 30px; 81 | text-decoration: none;; 82 | } 83 | 84 | a:hover { 85 | text-decoration: underline; 86 | } 87 | } 88 | 89 | .title-panel { 90 | border: 1px solid #ccc; 91 | border-radius: 5px; 92 | box-shadow: 0px 0px 6px 3px #eee; 93 | font-family: $title-font-family; 94 | height: 300px; 95 | padding: 20px; 96 | width: 80vh; 97 | 98 | .title { 99 | display: flex; 100 | 101 | h3 { 102 | font-size: 1.25rem; 103 | font-weight: 500; 104 | margin-top: 10px; 105 | margin-left: 10px; 106 | } 107 | } 108 | 109 | &.light { 110 | box-shadow: 0px 0px 6px 3px #eee; 111 | } 112 | 113 | &.dark { 114 | box-shadow: none; 115 | } 116 | 117 | &.md-blue { 118 | box-shadow: none; 119 | } 120 | } 121 | 122 | .banner { 123 | border-radius: 5px; 124 | display: flex; 125 | font-family: $form-font-family; 126 | justify-content: space-between; 127 | padding: 20px 20px; 128 | } 129 | 130 | .submit-panel { 131 | background-color: $panel-bg-color; 132 | border: 2px dashed $succees; 133 | color: $form-field-color; 134 | } 135 | 136 | .warning-panel { 137 | background-color: $panel-bg-color; 138 | border: 2px dashed $warning-dark; 139 | color: $form-field-color; 140 | } 141 | 142 | .banner-container { 143 | margin-left: 35px; 144 | margin-top: 50px; 145 | } 146 | 147 | -------------------------------------------------------------------------------- /src/sass/components/search.scss: -------------------------------------------------------------------------------- 1 | .search-field { 2 | border: 1px solid $title-bg-clr; 3 | border-radius: 10px; 4 | font-size: 15px; 5 | height: 30px; 6 | outline: none; 7 | width: 220px; 8 | } 9 | 10 | .search-btn { 11 | background-color: $title-bg-clr; 12 | border: none; 13 | border-top-right-radius: 5px; 14 | border-bottom-right-radius: 5px; 15 | color: $btn-color; 16 | font-size: 15px; 17 | height: 34px; 18 | padding: { 19 | top: 1px; 20 | bottom: 1px; 21 | } 22 | width: 80px; 23 | 24 | } -------------------------------------------------------------------------------- /src/sass/components/table.scss: -------------------------------------------------------------------------------- 1 | table { 2 | &.table { 3 | color: $form-field-color; 4 | font-family: $form-font-family; 5 | font-size: 16px; 6 | width: 100%; 7 | } 8 | 9 | tr:first-child { 10 | color: $form-label-color; 11 | text-align: left; 12 | } 13 | } -------------------------------------------------------------------------------- /src/sass/components/tabs.scss: -------------------------------------------------------------------------------- 1 | .tab-container { 2 | border-bottom: 2px solid #ccc; 3 | display: flex; 4 | margin-top: 20px; 5 | padding: { 6 | left: 40px; 7 | }; 8 | 9 | div { 10 | cursor: pointer; 11 | color: $form-label-color; 12 | font-family: $tab-font-family; 13 | margin-right: 20px; 14 | padding: { 15 | bottom: 10px; 16 | left: 10px; 17 | right: 10px; 18 | } 19 | 20 | &.active { 21 | border-bottom: 3px solid $border-left-color; 22 | color: $title-bg-clr; 23 | font-weight: bold; 24 | } 25 | } 26 | } 27 | 28 | .tab-page-container { 29 | margin-top: 20px; 30 | } -------------------------------------------------------------------------------- /src/sass/components/title.scss: -------------------------------------------------------------------------------- 1 | .header-container { 2 | background-color: $title-color; 3 | color: $title-bg-clr; 4 | display: flex; 5 | font-family: $title-font-family; 6 | justify-content: flex-end; 7 | font-size: 22px; 8 | padding: 15px; 9 | 10 | &.light { 11 | background-color: $title-color; 12 | color: $title-bg-clr; 13 | } 14 | 15 | &.dark { 16 | background-color: $title-bg-clr-drk; 17 | color: $title-color-drk; 18 | } 19 | 20 | &.md-blue { 21 | background-color: $title-bg-clr-mdblue; 22 | color: $title-color-drk; 23 | } 24 | } 25 | 26 | .page-title { 27 | display: flex; 28 | 29 | h1 { 30 | color: $page-title-color; 31 | font-size: 1.5rem; 32 | font-family: $title-font-family; 33 | font-weight: 400; 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | 39 | .icon-card { 40 | color: $icon-color; 41 | font-size: 45px; 42 | margin-right: 10px; 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/sass/components/tree.scss: -------------------------------------------------------------------------------- 1 | .rd3t-tree-container { 2 | svg { 3 | overflow: scroll; 4 | } 5 | } -------------------------------------------------------------------------------- /src/sass/theme/dark.scss: -------------------------------------------------------------------------------- 1 | body.dark 2 | { 3 | background: $body-clr-drk; 4 | color: $form-label-color-drk; 5 | } 6 | 7 | .dark { 8 | label { 9 | color: $form-label-color-drk; 10 | } 11 | 12 | h1 { 13 | color: $title-color-drk; 14 | } 15 | 16 | h3 { 17 | color: $page-title-color-dark; 18 | } 19 | 20 | h4 { 21 | color: $title-color-drk; 22 | } 23 | 24 | a { 25 | color: $form-label-color-drk; 26 | } 27 | 28 | .tab { 29 | color: $form-label-color-drk; 30 | 31 | &.active { 32 | color: $form-label-color-drk; 33 | } 34 | } 35 | 36 | .panel-box-wrapper { 37 | background-color: #fff; 38 | box-shadow: none; 39 | color: #000; 40 | 41 | a { 42 | color: $link-color; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/sass/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import './dark.scss'; 2 | @import './light.scss'; 3 | @import './md-blue.scss'; -------------------------------------------------------------------------------- /src/sass/theme/light.scss: -------------------------------------------------------------------------------- 1 | body.light { 2 | background: #fff; 3 | color: $form-label-color; 4 | } 5 | 6 | .light { 7 | label { 8 | color: $form-label-color; 9 | } 10 | 11 | h1 { 12 | color: $title-bg-clr; 13 | } 14 | 15 | h3 { 16 | color: $page-title-color; 17 | } 18 | 19 | h4 { 20 | color: $form-label-color; 21 | } 22 | 23 | .tab { 24 | color: $form-label-color; 25 | 26 | &.active { 27 | color: $title-bg-clr; 28 | } 29 | } 30 | 31 | .panel-box-wrapper { 32 | box-shadow: 1px 3px #eee; 33 | color: #000; 34 | } 35 | } -------------------------------------------------------------------------------- /src/sass/theme/md-blue.scss: -------------------------------------------------------------------------------- 1 | body.md-blue 2 | { 3 | background: $body-clr-mdblue; 4 | color: $form-label-color-drk; 5 | } 6 | 7 | .md-blue { 8 | label { 9 | color: $form-label-color-drk; 10 | } 11 | 12 | h1 { 13 | color: $title-color-drk; 14 | } 15 | 16 | h3 { 17 | color: $page-title-color-dark; 18 | } 19 | 20 | h4 { 21 | color: $form-label-color-drk; 22 | } 23 | 24 | a { 25 | color: $form-label-color-drk; 26 | } 27 | 28 | .tab { 29 | color: $form-label-color-drk; 30 | 31 | &.active { 32 | color: $form-label-color-drk; 33 | } 34 | } 35 | 36 | .panel-box-wrapper { 37 | background-color: #fff; 38 | box-shadow: none; 39 | color: #000; 40 | 41 | a { 42 | color: $link-color; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/sass/variables.scss: -------------------------------------------------------------------------------- 1 | $title-font-family: "Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif; 2 | $title-bg-clr: #2A3F54; 3 | $title-color: #ECF0F1; 4 | 5 | $body-clr-drk: #000; 6 | $title-bg-clr-drk: #191515; 7 | $title-color-drk: #aab5bf ; 8 | 9 | 10 | $body-clr-mdblue: #223c56; 11 | $title-bg-clr-mdblue: #081d31; 12 | 13 | 14 | $btn-color: #ECF0F1; 15 | $btn-bg-clr: rgba(255, 255, 255, 0.05); 16 | $btn-bg-clr-hover: rgba(255, 255, 255, 0.1); 17 | $btn-font-size: 17px; 18 | $btn-font-family: "Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif; 19 | $primary-btn-color: #1ABB9C; 20 | $cancel-btn-color: #73879C; 21 | $cancel-btn-color1: #545b62; 22 | $toobar-btn-color: #20a8d8; 23 | $button-dark: #404040; 24 | 25 | $border-left-color: #1ABB9C; 26 | 27 | $link-active-color: #1ABB9C; 28 | 29 | $sideNavWidth: 300px; 30 | $title-height: 30px; 31 | 32 | $page-title-color: #73879C; 33 | $page-title-color-dark: #dadfe4; 34 | 35 | $tab-font-family: "Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif; 36 | 37 | $form-font-family: "Helvetica Neue", Roboto, Arial, "Droid Sans", sans-serif; 38 | $form-field-color: #2A3F54; 39 | $form-label-color: #73879C; 40 | $form-label-color-drk: #aab5bf; 41 | 42 | $danger: #E74C3C; 43 | $warning: #f1c68e; 44 | $warning-dark: #f57826; 45 | $warning-btn: #d34836; 46 | $succees: #17b395; 47 | 48 | 49 | 50 | $link-color: #6b5b95; 51 | 52 | $link-clr-drk: #bca8f1; 53 | 54 | $attribute-type: ( 55 | "string": #2FA4E7, 56 | "boolean": #DD5600, 57 | "object": #73A839, 58 | "number": #9A1750, 59 | "array": #6610f2, 60 | ); 61 | 62 | $form-border-color: #2A3F54; 63 | 64 | $dark-color: #404040; 65 | 66 | $panel-bg-color: #F5F5F5; 67 | 68 | $icon-color: #b0c1d2; 69 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import reducers from './reducers'; 3 | import thunk from 'redux-thunk'; 4 | import { composeWithDevTools } from 'redux-devtools-extension'; 5 | 6 | const devToolEnhancer = process.env.NODE_ENV !== 'production' ? composeWithDevTools : compose; 7 | 8 | const enhancer = devToolEnhancer(applyMiddleware(thunk)); 9 | 10 | export default () => { 11 | const store = createStore(reducers, enhancer); 12 | if (module.hot) { 13 | module.hot.accept('./reducers', () => { 14 | store.replaceReducer(reducers); 15 | }); 16 | } 17 | return store; 18 | } -------------------------------------------------------------------------------- /src/utils/stringutils.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const isContains = (str1, str2) => { 4 | const lowercasestr1 = str1.toLowerCase(); 5 | const lowercasestr2 = str2.toLowerCase(); 6 | return lowercasestr1.indexOf(lowercasestr2) > -1; 7 | } -------------------------------------------------------------------------------- /src/utils/transform.js: -------------------------------------------------------------------------------- 1 | import { has } from 'lodash/object'; 2 | import { isArray } from 'lodash/lang'; 3 | import { join } from 'lodash/array'; 4 | 5 | const nodeSvgShape = { 6 | shape: 'circle', 7 | shapeProps: { 8 | fill: '#1ABB9C', 9 | r: 10, 10 | }, 11 | }; 12 | 13 | const mapFactToChildren = (fact) => { 14 | if (has(fact, 'fact') && has(fact, 'operator') && has(fact, 'value')) { 15 | let value = fact.value; 16 | let attributes = {}; 17 | if (isArray(fact.value)) { 18 | value = join(fact.value, ','); 19 | } 20 | 21 | attributes[fact.operator] = value; 22 | 23 | if (fact.path) { 24 | attributes['path'] = fact.path; 25 | } 26 | return ({name: fact.fact, attributes}); 27 | } 28 | return undefined; 29 | }; 30 | 31 | const mapParentNode = (name) => { 32 | return ({name, nodeSvgShape, children : []}); 33 | }; 34 | 35 | //global variable to determine the depth 36 | let depthCount; 37 | 38 | const mapConditionsToChildren = (condition={}, depth) => { 39 | const parentNode = has(condition, 'all') ? 'all' : 'any'; 40 | const node = mapParentNode(parentNode); 41 | const childrenNode = condition[parentNode] && condition[parentNode].map(facts => { 42 | if (has(facts, 'fact')) { 43 | return mapFactToChildren(facts); 44 | } else { 45 | depthCount = depth > depthCount ? depth : depthCount; 46 | return mapConditionsToChildren(facts, depth + 1); 47 | } 48 | }); 49 | node.children = childrenNode; 50 | return node; 51 | }; 52 | 53 | export const transformRuleToTree = (conditions = []) => { 54 | depthCount = 0; 55 | if (isArray(conditions)) { 56 | return conditions.map((condition) => { 57 | depthCount = 0; 58 | return { node: mapConditionsToChildren(condition.conditions, 1), depthCount, index: condition.index, event: condition.event }; 59 | }); 60 | } 61 | return { node: mapConditionsToChildren(conditions.conditions, 1), depthCount, index: 0, event: conditions.event}; 62 | }; 63 | 64 | const mapChildNodeToFacts = (children) => { 65 | const fact = { fact: children.name }; 66 | Object.keys(children.attributes).forEach((key) => { 67 | if (key === 'path') { 68 | fact['path'] = children.attributes.path; 69 | } else { 70 | fact['operator'] = key; 71 | let value; 72 | if (String(children.attributes[key]).indexOf(',') > -1) { 73 | value = children.attributes[key].split(','); 74 | } else { 75 | value = children.attributes[key]; 76 | } 77 | fact['value'] = value; 78 | } 79 | }); 80 | return fact; 81 | } 82 | 83 | const mapNodeToCondition = (node) => { 84 | const parentNode = { [node.name]: [] }; 85 | 86 | if (node.children && node.children.length > 0) { 87 | const facts = node.children.map((childNode) => { 88 | if (childNode.name !== 'all' && childNode.name !== 'any') { 89 | return mapChildNodeToFacts(childNode); 90 | } else { 91 | return mapNodeToCondition(childNode); 92 | } 93 | }) 94 | parentNode[node.name] = facts; 95 | } 96 | return parentNode; 97 | } 98 | 99 | 100 | export const transformTreeToRule = (node = {}, outcome, params) => { 101 | return ({conditions: mapNodeToCondition(node), event: { type: outcome.value, params }}); 102 | } -------------------------------------------------------------------------------- /src/utils/treeutils.js: -------------------------------------------------------------------------------- 1 | import { findIndex } from 'lodash/array'; 2 | 3 | const mapNodeName = (node) => 4 | ({ name: node.name, depth: node.depth, id: node.id }); 5 | 6 | const getNodeIndex = (parentNode, childNode) => { 7 | if (parentNode && parentNode.children && parentNode.children.length > 0) { 8 | return findIndex(parentNode.children, { 'id': childNode.id }); 9 | } 10 | return undefined; 11 | } 12 | 13 | export const getNodeDepthDetails = (node, result = []) => { 14 | const obj = mapNodeName(node); 15 | // eslint-disable-next-line no-constant-condition 16 | while(true) { 17 | if(node.depth > 0) { 18 | const index = getNodeIndex(node.parent, obj); 19 | obj['index'] = index; 20 | result.push(obj); 21 | return getNodeDepthDetails(node.parent, result); 22 | } 23 | break; 24 | } 25 | return result; 26 | } 27 | 28 | const iterateNode = (node, depth) => { 29 | 30 | if (node.children && node.children.length > 0) { 31 | depth = depth + 1; 32 | node.children.forEach(childNode => { 33 | depth = iterateNode(childNode, depth); 34 | }) 35 | 36 | } 37 | return depth; 38 | } 39 | 40 | export const getNodeDepth = node => iterateNode(node, 0); -------------------------------------------------------------------------------- /src/validations/attribute-validations.js: -------------------------------------------------------------------------------- 1 | 2 | export const validateAttribute = (attribute) => { 3 | const error = {}; 4 | if (!attribute.name) { 5 | error.name = 'Please specify the attribute name' 6 | } 7 | 8 | if (!attribute.type) { 9 | error.type = 'Please specify the attribute type' 10 | } 11 | 12 | return error; 13 | } 14 | 15 | export default function attributeValidations(attribute) { 16 | return validateAttribute(attribute); 17 | } 18 | -------------------------------------------------------------------------------- /src/validations/decision-validation.js: -------------------------------------------------------------------------------- 1 | 2 | export const validateOutcome = (outcome) => { 3 | const error = {}; 4 | 5 | if (!outcome.value) { 6 | error.value = 'Please specify the outcome value' 7 | } 8 | 9 | return error; 10 | } 11 | 12 | const isEmpty = (val) => { 13 | if(!val) { 14 | return true; 15 | } else if (!val.trim()) { 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | const fieldValidationByType = (value, type, operator) => { 22 | switch(type) { 23 | case 'string': 24 | return value.indexOf(',') === -1; 25 | case 'number': { 26 | const re = RegExp('[+-]?([0-9]*[.])?[0-9]+'); 27 | if (re.test(value)) { 28 | return !(isNaN(Number(value))); 29 | } 30 | return re.test(value); 31 | } 32 | case 'array': { 33 | if (operator === 'doesNotContain' || operator === 'contains') { 34 | return value.indexOf(',') === -1; 35 | } else { 36 | const arrValues = value.split(','); 37 | if (arrValues && arrValues.length > 0) { 38 | return !arrValues.some(v => isEmpty(v)) 39 | } else { 40 | return false; 41 | } 42 | } 43 | } 44 | default: 45 | return true; 46 | } 47 | } 48 | 49 | export const validateAttribute = (attribute, attributes) => { 50 | const error = {}; 51 | if (isEmpty(attribute.operator)) { 52 | error.operator = 'Please specify the operator type' 53 | } 54 | 55 | if (isEmpty(attribute.value)) { 56 | error.value = 'Please specify the attribute value' 57 | } else { 58 | if (attribute.name) { 59 | const attProps = attributes.find(att => att.name === attribute.name); 60 | if(attProps && attProps.type) { 61 | if (!fieldValidationByType(attribute.value, attProps.type, attribute.operator)) { 62 | error.value = 'Please specify the valid attribute value' ; 63 | } 64 | 65 | } 66 | } 67 | } 68 | 69 | if (isEmpty(attribute.name)) { 70 | error.name = 'Please specify the attribute name' 71 | } 72 | 73 | return error; 74 | } 75 | 76 | export default function decisionValidations(node={}, outcome) { 77 | const error = {node: {}, outcome: {}}; 78 | error.outcome = validateOutcome(outcome); 79 | const validCase = node.children && node.children.length > 0; 80 | 81 | if (!validCase) { 82 | error.formError = 'Please specify atlease one condition'; 83 | } else if (Object.keys(error.outcome).length > 0){ 84 | error.formError = 'Please specify valid output values'; 85 | } 86 | return error; 87 | } -------------------------------------------------------------------------------- /src/validations/rule-validation.js: -------------------------------------------------------------------------------- 1 | import { Engine } from 'json-rules-engine'; 2 | 3 | 4 | export const processEngine = (fact, conditions) => { 5 | const engine = new Engine(conditions); 6 | 7 | return engine.run(fact) 8 | .then(results => { 9 | return results.events 10 | }) 11 | .catch((e) => { 12 | console.error('Problem occured when processing the rules', e); 13 | return Promise.reject({ error: e.message }); 14 | }); 15 | }; 16 | 17 | export const validateRuleset = async (facts, conditions) => { 18 | const result = await processEngine(facts, conditions); 19 | return result; 20 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const path = require('path'); 4 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer-sunburst').BundleAnalyzerPlugin; 5 | 6 | 7 | module.exports = (env, arg) => ({ 8 | entry: ['./src/app.js', './src/sass/base.scss'], 9 | output: { 10 | filename: "main.bundle.js", 11 | path: path.resolve(__dirname, 'dist') 12 | }, 13 | mode: arg.mode != 'production' ? 'development' : 'production', 14 | devtool: arg.mode != 'production' ? 'eval-source-map' : 'nosources-source-map', 15 | module: { 16 | rules: [{ 17 | test: /\.(js|jsx)$/, 18 | use: ['babel-loader'], 19 | exclude: /node_modules/, 20 | }, 21 | { 22 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 23 | loader: "url-loader", 24 | options: { 25 | name: '[path][name].[ext]', 26 | limit: 10000, 27 | } 28 | }, 29 | { 30 | test: /\.(ttf|eot|svg|png|jpg|jpeg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 31 | loader: "file-loader", 32 | options: { 33 | name: '[path][name].[ext]', 34 | } 35 | }, 36 | { 37 | test: /\.scss$/, 38 | use: [ 39 | { loader: MiniCssExtractPlugin.loader }, 40 | { loader: "css-loader" }, 41 | { 42 | loader: "sass-loader", 43 | options: { 44 | sourceMap: false 45 | } 46 | }], 47 | exclude: /node_modules/, 48 | }, { 49 | test: /\.css$/, 50 | use: ['style-loader', 'css-loader'], 51 | } 52 | ] 53 | }, 54 | resolve: { 55 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 56 | alias: { 57 | 'react-dom': '@hot-loader/react-dom', 58 | }, 59 | fallback: { "vm": false } 60 | }, 61 | plugins: [ 62 | new HtmlWebPackPlugin({ 63 | template: 'index.html', 64 | favicon: "./assets/icons/rule.ico" 65 | }), 66 | new MiniCssExtractPlugin({ 67 | filename: '[name].css', 68 | chunkFilename: '[id].css' 69 | }), 70 | /* new BundleAnalyzerPlugin({ 71 | analyzerMode: 'static', 72 | }), */ 73 | ] 74 | }); 75 | --------------------------------------------------------------------------------