configurator: add new configurator
add new configurator Tracked-On: #6691 Signed-off-by: Weiyi Feng <weiyix.feng@intel.com> (cherry picked from commit 1232a7229af7ed60706cc725acf955ccbd8ca035)
3
.gitignore
vendored
@ -6,5 +6,8 @@ build
|
||||
*.warnings
|
||||
*.pyc
|
||||
/offline-tools/
|
||||
.idea
|
||||
venv
|
||||
*.egg-info
|
||||
/misc/config_tools/dist
|
||||
/misc/config_tools/configurator/src-tauri/tauri.conf.json
|
9
Makefile
@ -80,6 +80,8 @@ HV_CFG_LOG = $(HV_OUT)/cfg.log
|
||||
VM_CONFIGS_DIR = $(T)/misc/config_tools
|
||||
ASL_COMPILER ?= $(shell which iasl)
|
||||
DPKG_BIN ?= $(shell which dpkg)
|
||||
YARN_BIN ?= $(shell which yarn)
|
||||
CARGO_BIN ?= $(shell which cargo)
|
||||
|
||||
.PHONY: all hypervisor devicemodel tools life_mngr doc
|
||||
all: hypervisor devicemodel tools
|
||||
@ -113,6 +115,13 @@ board_inspector:
|
||||
echo -e "The 'dpkg' utility is not available. Unable to create Debian package for board_inspector."; \
|
||||
fi
|
||||
|
||||
configurator:
|
||||
@if [ -x "$(YARN_BIN)" ] && [ -x "$(CARGO_BIN)" ]; then \
|
||||
python3 misc/packaging/gen_acrn_deb.py configurator $(ROOT_OUT) ; \
|
||||
else \
|
||||
echo -e "'yarn' or 'cargo' utility is not available. Unable to create Debian package for configurator."; \
|
||||
fi
|
||||
|
||||
hypervisor: hvdefconfig
|
||||
$(MAKE) $(HV_MAKEOPTS)
|
||||
@echo -e "ACRN Configuration Summary:" > $(HV_CFG_LOG)
|
||||
|
27
misc/config_tools/configurator/.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
build
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# TS temp file
|
||||
tauri-plugin.js.map
|
||||
tauri-plugin.js
|
79
misc/config_tools/configurator/README.md
Normal file
@ -0,0 +1,79 @@
|
||||
# ACRN Configurator
|
||||
|
||||
This version based on tauri, WIP.
|
||||
|
||||
## Features
|
||||
|
||||
### Support Platforms
|
||||
|
||||
- [x] Linux (.deb, AppImage)
|
||||
- [x] Windows 7,8,10 (.exe, .msi)
|
||||
- [x] macOS (.app, .dmg)
|
||||
|
||||
|
||||
## Setting Up
|
||||
|
||||
### 1. Install System Dependencies
|
||||
|
||||
Please follow [this guide](https://tauri.studio/docs/getting-started/prerequisites)
|
||||
to install system dependencies **(including yarn)**.
|
||||
|
||||
### 2. Clone Project And Install Project Dependencies.
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
sudo apt install git
|
||||
git clone https://github.com/projectacrn/acrn-hypervisor
|
||||
cd acrn-hypervisor/misc/config_tools/configurator
|
||||
python3 -m pip install -r requirements.txt
|
||||
yarn
|
||||
```
|
||||
|
||||
#### Windows && macOS
|
||||
|
||||
Similar to Linux.
|
||||
|
||||
On macOS, you may need to install git and python3 via `brew`.
|
||||
|
||||
In Windows environment maybe you need to install git and python3 via chocolatey or manually,
|
||||
and replace the command line `python3` with `py -3`.
|
||||
|
||||
### 3. How To Build
|
||||
|
||||
#### Linux
|
||||
|
||||
Run this command in the acrn-hypervisor directory.
|
||||
|
||||
```shell
|
||||
make configurator
|
||||
```
|
||||
|
||||
#### Windows/macOS
|
||||
|
||||
Run follow command in the 'acrn-hypervisor' directory.
|
||||
|
||||
```shell
|
||||
python3 misc/config_tools/scenario_config/schema_slicer.py
|
||||
python3 misc/config_tools/scenario_config/xs2js.py
|
||||
cd misc/config_tools/configurator
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 4. How To Run
|
||||
|
||||
#### Linux
|
||||
|
||||
Run this command in the acrn-hypervisor directory.
|
||||
|
||||
```shell
|
||||
sudo apt install ./build/acrn-configurator_*.deb
|
||||
acrn-configurator
|
||||
```
|
||||
|
||||
#### Windows/macOS
|
||||
|
||||
You can find msi(Windows)/dmg(macOS) folder under the
|
||||
`misc/config_tools/configurator/src-tauri/target/release/bundle`
|
||||
|
||||
directory, the installer in the folder.
|
24
misc/config_tools/configurator/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Vite App</title>
|
||||
<script src="https://cdn.jsdelivr.net/pyodide/v0.19.1/full/pyodide.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript">
|
||||
async function main() {
|
||||
window.pyodide = await loadPyodide({
|
||||
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.19.1/full/"
|
||||
})
|
||||
pyodide.loadPackage(['micropip', 'lxml', 'beautifulsoup4'])
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
47
misc/config_tools/configurator/package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "acrn-configurator",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"author": {
|
||||
"name": "Feng, Weiyi",
|
||||
"email": "weiyix.feng@intel.com",
|
||||
"url": "https://github.com/Weiyi-Feng"
|
||||
},
|
||||
"description": "ACRN Configurator",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.0.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.17",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@rjsf/core": "^4.0.1",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@tauri-apps/api": "^1.0.0-rc.1",
|
||||
"bootstrap": "^5.1.3",
|
||||
"js-base64": "^3.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "2",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.1.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-router": "^6.2.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"vconsole": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.0.0-rc.5",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/node": "^17.0.21",
|
||||
"@vitejs/plugin-react": "^1.0.7",
|
||||
"sass": "^1.49.9",
|
||||
"vite": "^2.8.0"
|
||||
}
|
||||
}
|
7
misc/config_tools/configurator/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
lxml
|
||||
xmltodict
|
||||
xmlschema
|
||||
defusedxml
|
||||
requests
|
||||
bs4
|
||||
sphinx
|
14
misc/config_tools/configurator/src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
WixTools
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
config.json
|
||||
bundle.json
|
||||
|
||||
# ts temp file
|
||||
types/*.js
|
||||
types/*.map
|
4243
misc/config_tools/configurator/src-tauri/Cargo.lock
generated
Normal file
33
misc/config_tools/configurator/src-tauri/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "acrn-configurator"
|
||||
version = "0.1.0"
|
||||
description = "ACRN Configurator"
|
||||
authors = ["Feng, Weiyi <weiyix.feng@intel.com>"]
|
||||
license = "BSD"
|
||||
repository = "https://github.com/projectacrn/acrn-hypervisor"
|
||||
default-run = "acrn-configurator"
|
||||
edition = "2021"
|
||||
rust-version = "1.57"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-rc.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { git = "https://github.com/Weiyi-Feng/tauri.git", features = ["api-all", "cli"] }
|
||||
log = "0.4.14"
|
||||
env_logger = "0.9.0"
|
||||
glob = "0.3.0"
|
||||
dirs = "4.0.0"
|
||||
itertools = "0.10.3"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
3
misc/config_tools/configurator/src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
BIN
misc/config_tools/configurator/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
misc/config_tools/configurator/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
misc/config_tools/configurator/src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 974 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 903 B |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.4 KiB |
BIN
misc/config_tools/configurator/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
misc/config_tools/configurator/src-tauri/icons/icon.icns
Normal file
BIN
misc/config_tools/configurator/src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
misc/config_tools/configurator/src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
13
misc/config_tools/configurator/src-tauri/rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2021"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
316
misc/config_tools/configurator/src-tauri/src/configurator.rs
Normal file
@ -0,0 +1,316 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::fs;
|
||||
use std::ops::Add;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use glob::{glob_with, MatchOptions};
|
||||
use itertools::Itertools;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
#[repr(u16)]
|
||||
#[non_exhaustive]
|
||||
pub enum HistoryType {
|
||||
WorkingFolder = 1,
|
||||
Board,
|
||||
Scenario,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct History {
|
||||
pub working_folder: Vec<String>,
|
||||
pub board_file: Vec<String>,
|
||||
pub scenario_file: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConfigData {
|
||||
pub history: History,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Configurator {
|
||||
pub config_write_enable: bool,
|
||||
pub config_path: PathBuf,
|
||||
|
||||
pub config_data: ConfigData,
|
||||
pub working_folder: String,
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub fn write_file(path: PathBuf, content: String) -> Result<(), String> {
|
||||
fs::write(path, content).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
impl ConfigData {
|
||||
fn new() -> ConfigData {
|
||||
let history = History { working_folder: vec![], board_file: vec![], scenario_file: vec![] };
|
||||
ConfigData { history }
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> String {
|
||||
serde_json::to_string(&self).unwrap_or_else(|_| {
|
||||
let default = ConfigData::new();
|
||||
ConfigData::serialize(&default)
|
||||
})
|
||||
}
|
||||
|
||||
/// deserialize data
|
||||
fn deserialize(config_json: String) -> Result<ConfigData, String> {
|
||||
match serde_json::from_str(&config_json.to_string()) {
|
||||
Ok(config_data) => Ok(config_data),
|
||||
Err(e) => Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Configurator {
|
||||
pub fn new() -> Self {
|
||||
match Self::ensure_config_file() {
|
||||
Ok(config_file_path) => {
|
||||
// read config.json
|
||||
Self::init(config_file_path)
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("get config file path error! error: {}", e.to_string());
|
||||
log::warn!("Use blank config and disable config write to start configurator.");
|
||||
Self {
|
||||
config_write_enable: false,
|
||||
config_path: Path::new(".").to_path_buf(),
|
||||
config_data: ConfigData::new(),
|
||||
working_folder: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_config_file() -> Result<PathBuf, String> {
|
||||
// get config_dir or home_dir path
|
||||
// Todo: 讨论fallback逻辑是否可行
|
||||
let config_base = match dirs::config_dir() {
|
||||
None => {
|
||||
log::info!("get config_dir error! fallback to get home_dir.");
|
||||
match dirs::home_dir() {
|
||||
None => {
|
||||
return Err("get config_dir and home_dir error!".to_string());
|
||||
}
|
||||
Some(path) => path
|
||||
}
|
||||
}
|
||||
Some(path) => path
|
||||
};
|
||||
|
||||
|
||||
// get acrn-configurator dir path and check it exist
|
||||
let config_dir = config_base.join(".acrn-configurator");
|
||||
log::info!("current config_dir is {}.", config_dir.to_str().unwrap());
|
||||
if !config_dir.is_dir() {
|
||||
match fs::create_dir(&config_dir) {
|
||||
Err(e) => {
|
||||
//Todo: 明确无法创建 .acrn-configurator 文件夹时的处理逻辑
|
||||
log::warn!("Create configurator config dir failed, {}", e.to_string());
|
||||
return Err(e.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get config.json file path and check it exist
|
||||
let default_config_path = config_dir.join("config.json");
|
||||
if !default_config_path.is_file() {
|
||||
let empty_config = ConfigData::new();
|
||||
match fs::write(&default_config_path, empty_config.serialize()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(e.to_string())
|
||||
};
|
||||
}
|
||||
Ok(default_config_path)
|
||||
}
|
||||
|
||||
pub fn init(config_file_path: PathBuf) -> Configurator {
|
||||
let config_json = match fs::read_to_string(&config_file_path) {
|
||||
Ok(data) => { data }
|
||||
Err(e) => {
|
||||
log::warn!("read config error! error: {}", e.to_string());
|
||||
log::warn!("Use default blank config to start due to read config failed.");
|
||||
return Configurator {
|
||||
config_write_enable: false,
|
||||
config_path: config_file_path,
|
||||
config_data: ConfigData::new(),
|
||||
working_folder: "".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let config_data = match ConfigData::deserialize(config_json) {
|
||||
Ok(config_data) => {
|
||||
log::info!("success load config: {}", config_data.serialize());
|
||||
config_data
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Deserialize json data error! error: {}", e);
|
||||
log::warn!("Use default blank config to start due to deserialize config failed.");
|
||||
ConfigData::new()
|
||||
}
|
||||
};
|
||||
log::info!("Using config: {}", config_data.serialize());
|
||||
Configurator {
|
||||
config_write_enable: true,
|
||||
config_path: config_file_path,
|
||||
config_data,
|
||||
working_folder: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn save_config(&self) {
|
||||
if !self.config_write_enable {
|
||||
return;
|
||||
}
|
||||
match fs::write(&self.config_path, self.config_data.serialize()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("Write config error! error:{}",e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_history(&mut self, history_type: HistoryType, path: &Path) {
|
||||
let path_string: String = path.to_string_lossy().parse().unwrap();
|
||||
match history_type {
|
||||
HistoryType::WorkingFolder => {
|
||||
self.config_data.history.working_folder.insert(0, path_string);
|
||||
self.config_data.history.working_folder = self.config_data.history.working_folder.clone().into_iter().unique().collect()
|
||||
}
|
||||
HistoryType::Board => {
|
||||
self.config_data.history.board_file.insert(0, path_string);
|
||||
self.config_data.history.board_file = self.config_data.history.board_file.clone().into_iter().unique().collect()
|
||||
}
|
||||
HistoryType::Scenario => {
|
||||
self.config_data.history.scenario_file.insert(0, path_string);
|
||||
self.config_data.history.scenario_file = self.config_data.history.scenario_file.clone().into_iter().unique().collect()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_history(&self, history_type: HistoryType) -> &[String] {
|
||||
match history_type {
|
||||
HistoryType::WorkingFolder => {
|
||||
self.config_data.history.working_folder.borrow()
|
||||
}
|
||||
HistoryType::Board => {
|
||||
self.config_data.history.board_file.borrow()
|
||||
}
|
||||
HistoryType::Scenario => {
|
||||
self.config_data.history.scenario_file.borrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn force_reset(&mut self) {
|
||||
self.config_data = ConfigData::new();
|
||||
self.save_config()
|
||||
}
|
||||
|
||||
pub fn set_working_folder(&mut self, working_folder: String) {
|
||||
self.working_folder = working_folder
|
||||
}
|
||||
|
||||
pub fn write_board(&self, board_name: String, board_xml_string: String) -> Result<(), String> {
|
||||
let options = MatchOptions {
|
||||
case_sensitive: false,
|
||||
..Default::default()
|
||||
};
|
||||
let pattern = self.working_folder.clone().add("/.*\\.board\\.xml");
|
||||
let files = match glob_with(&pattern, options).map_err(|e| e.to_string()) {
|
||||
Ok(files) => { files }
|
||||
Err(e) => return Err(e.to_string())
|
||||
};
|
||||
for entry in files {
|
||||
match entry {
|
||||
Ok(filepath) => {
|
||||
match fs::remove_file(&filepath) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let err_msg = format!(
|
||||
"Can not delete file:{} error: {}",
|
||||
filepath.to_str().unwrap_or_else(|| "").to_string(), e.to_string()
|
||||
);
|
||||
log::warn!("{}",err_msg);
|
||||
return Err(err_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("find old board error! error:{}", e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let board_basename = board_name.add(".board.xml");
|
||||
let board_xml_path = Path::new(&self.working_folder).join(board_basename);
|
||||
write_file(board_xml_path, board_xml_string)
|
||||
}
|
||||
}
|
||||
|
||||
static mut WORKING_FOLDER: String = String::new();
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_history(history_type: HistoryType) -> Result<String, ()> {
|
||||
let configurator = Configurator::new();
|
||||
let history = serde_json::to_string(configurator.get_history(history_type)).unwrap_or_else(|_| String::from("[]"));
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_history(history_type: HistoryType, path: String) -> Result<(), &'static str> {
|
||||
let path_buf = Path::new(&path);
|
||||
if !(path_buf.is_dir() || path_buf.is_file()) {
|
||||
return Err("Not a validate dir or file path.");
|
||||
}
|
||||
let mut configurator = Configurator::new();
|
||||
configurator.add_history(history_type, path_buf);
|
||||
configurator.save_config();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_working_folder(working_folder: String) -> Result<(), ()> {
|
||||
unsafe {
|
||||
WORKING_FOLDER = working_folder;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub fn write_board(board_name: String, contents: String) -> Result<(), String> {
|
||||
let mut configurator = Configurator::new();
|
||||
unsafe {
|
||||
configurator.set_working_folder(WORKING_FOLDER.clone());
|
||||
}
|
||||
configurator.write_board(board_name, contents)
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub fn force_reset() -> Result<(), ()> {
|
||||
let mut configurator = Configurator::new();
|
||||
configurator.force_reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_home() -> Result<String, ()> {
|
||||
match dirs::home_dir() {
|
||||
None => {
|
||||
Ok(String::new())
|
||||
}
|
||||
Some(path) => {
|
||||
Ok(path.to_str().unwrap().to_string())
|
||||
}
|
||||
}
|
||||
}
|
102
misc/config_tools/configurator/src-tauri/src/filesystem.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use serde::Serialize;
|
||||
use std::fs::{self, File};
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_read_text_file(path: &str) -> Result<String, String> {
|
||||
let mut file = File::open(path).map_err(|e| e.to_string())?;
|
||||
let mut contents = String::new();
|
||||
file
|
||||
.read_to_string(&mut contents)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_write_text_file(path: &str, contents: &str) -> Result<(), String> {
|
||||
let mut file = File::create(path).map_err(|e| e.to_string())?;
|
||||
file
|
||||
.write_all(contents.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_read_binary_file(path: &str) -> Result<Vec<u8>, String> {
|
||||
let mut file = File::open(path).map_err(|e| e.to_string())?;
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents).map_err(|e| e.to_string())?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_write_binary_file(path: &str, contents: &[u8]) -> Result<(), String> {
|
||||
let mut file = File::create(path).map_err(|e| e.to_string())?;
|
||||
file.write_all(contents).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DirEntry {
|
||||
path: String,
|
||||
children: Option<Vec<DirEntry>>,
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Path>>(
|
||||
path: P,
|
||||
recursive: bool,
|
||||
) -> io::Result<Vec<DirEntry>> {
|
||||
let path = path.as_ref();
|
||||
let mut entries = Vec::new();
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path().to_str().unwrap().to_string();
|
||||
let children = if recursive && entry.file_type()?.is_dir() {
|
||||
Some(read_dir(&path, true)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
entries.push(DirEntry { path, children });
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_read_dir(
|
||||
path: &str,
|
||||
recursive: bool,
|
||||
) -> Result<Vec<DirEntry>, String> {
|
||||
read_dir(path, recursive).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_rename(from: &str, to: &str) -> Result<(), String> {
|
||||
fs::rename(from, to).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_delete_file(path: &str) -> Result<(), String> {
|
||||
fs::remove_file(path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_delete_dir(path: &str) -> Result<(), String> {
|
||||
fs::remove_dir_all(path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_create_dir(path: &str) -> Result<(), String> {
|
||||
fs::create_dir(path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_is_file(path: &str) -> bool {
|
||||
fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn fs_is_dir(path: &str) -> bool {
|
||||
fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
|
||||
}
|
80
misc/config_tools/configurator/src-tauri/src/main.rs
Normal file
@ -0,0 +1,80 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod filesystem;
|
||||
mod configurator;
|
||||
|
||||
use log::*;
|
||||
|
||||
use tauri::{api::cli::get_matches, utils::config::CliConfig, PackageInfo, RunEvent};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
// Init context
|
||||
let context = tauri::generate_context!();
|
||||
|
||||
// Build app instance and run
|
||||
let app = tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
filesystem::fs_rename,
|
||||
filesystem::fs_read_dir,
|
||||
filesystem::fs_read_text_file,
|
||||
filesystem::fs_read_binary_file,
|
||||
filesystem::fs_write_text_file,
|
||||
filesystem::fs_write_binary_file,
|
||||
filesystem::fs_read_dir,
|
||||
filesystem::fs_delete_file,
|
||||
filesystem::fs_delete_dir,
|
||||
filesystem::fs_create_dir,
|
||||
filesystem::fs_is_file,
|
||||
filesystem::fs_is_dir,
|
||||
configurator::get_history,
|
||||
configurator::add_history,
|
||||
configurator::set_working_folder,
|
||||
configurator::write_board,
|
||||
configurator::force_reset,
|
||||
configurator::get_home
|
||||
])
|
||||
.setup(|app| {
|
||||
// Handle cli cmdline
|
||||
let app_config = app.config();
|
||||
let cli_config = app_config.tauri.cli.as_ref().unwrap();
|
||||
let package_info = app.package_info();
|
||||
handle_cli_arg_data(&cli_config, &package_info);
|
||||
Ok(())
|
||||
})
|
||||
.build(context)
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|_app, event| {
|
||||
if let RunEvent::Exit = event {
|
||||
info!("Received Exit event");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fn handle_cli_arg_data(cli_config: &CliConfig, pkg_info: &PackageInfo) {
|
||||
match get_matches(cli_config, pkg_info) {
|
||||
Ok(matches) => {
|
||||
let mut exit_flag = false;
|
||||
if let Some(arg_data) = matches.args.get("help") {
|
||||
println!("{}", arg_data.value.as_str().unwrap_or("No help available"));
|
||||
exit_flag = true;
|
||||
}
|
||||
if let Some(arg_data) = matches.args.get("version") {
|
||||
println!("{}", arg_data.value.as_str().unwrap_or("No version data available"));
|
||||
exit_flag = true
|
||||
}
|
||||
if exit_flag {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e.to_string());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
78
misc/config_tools/configurator/src-tauri/tauri.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"$schema": "types/schema.json",
|
||||
"package": {
|
||||
"productName": "acrn-configurator",
|
||||
"version": "../package.json"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../build",
|
||||
"devPath": "http://localhost:3000",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"tauri": {
|
||||
"cli": {
|
||||
"args": [
|
||||
{
|
||||
"name": "debug",
|
||||
"takesValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.tauri.dev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "",
|
||||
"longDescription": "",
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"useBootstrapper": false
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"providerShortName": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "ACRN Configurator",
|
||||
"width": 1366,
|
||||
"height": 768,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
114
misc/config_tools/configurator/src-tauri/types/config.ts
Normal file
@ -0,0 +1,114 @@
|
||||
export default interface Config {
|
||||
"$schema"?: string
|
||||
package?: PackageConfig
|
||||
tauri: TauriConfig
|
||||
build: BuildConfig
|
||||
plugins?: {}
|
||||
}
|
||||
|
||||
interface PackageConfig {
|
||||
/// App name.
|
||||
productName?: string,
|
||||
/// App version. It is a semver version number or a path to a `package.json` file contaning the `version` field.
|
||||
version?: string
|
||||
}
|
||||
|
||||
interface TauriConfig {
|
||||
pattern?: {
|
||||
/**
|
||||
* Brownfield pattern.
|
||||
*/
|
||||
Brownfield: any,
|
||||
/// Isolation pattern. Recommended for security purposes.
|
||||
Isolation: {
|
||||
/// The dir containing the index.html file that contains the secure isolation application.
|
||||
dir: string,
|
||||
},
|
||||
}
|
||||
|
||||
windows: WindowConfig[]
|
||||
/**
|
||||
* app's CLI definition
|
||||
*/
|
||||
cli?: {}
|
||||
bundle?: {}
|
||||
allowlist?: {}
|
||||
security?: {}
|
||||
updater?: {}
|
||||
system_tray?: {}
|
||||
macos_private_api?: {}
|
||||
|
||||
}
|
||||
|
||||
interface WindowConfig {
|
||||
/// The window identifier. It must be alphanumeric.
|
||||
label?: string,
|
||||
/// The window webview URL.
|
||||
url?: string,
|
||||
/// Whether the file drop is enabled or not on the webview. By default it is enabled.
|
||||
///
|
||||
/// Disabling it is required to use drag and drop on the frontend on Windows.
|
||||
file_drop_enabled?: boolean,
|
||||
/// Whether or not the window starts centered or not.
|
||||
center?: boolean,
|
||||
/// The horizontal position of the window's top left corner
|
||||
x?: number,
|
||||
/// The vertical position of the window's top left corner
|
||||
y?: number,
|
||||
/// The window width.
|
||||
width?: number,
|
||||
/// The window height.
|
||||
height?: number,
|
||||
/// The min window width.
|
||||
min_width?: number,
|
||||
/// The min window height.
|
||||
min_height?: number,
|
||||
/// The max window width.
|
||||
max_width?: number,
|
||||
/// The max window height.
|
||||
max_height?: number,
|
||||
/// Whether the window is resizable or not.
|
||||
resizable?: boolean,
|
||||
/// The window title.
|
||||
title?: string,
|
||||
/// Whether the window starts as fullscreen or not.
|
||||
fullscreen?: boolean,
|
||||
/// Whether the window will be initially hidden or focused.
|
||||
focus?: boolean,
|
||||
/// Whether the window is transparent or not.
|
||||
///
|
||||
/// Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > tauri > macosPrivateApi`.
|
||||
/// WARNING: Using private APIs on `macOS` prevents your application from being accepted for the `App Store`.
|
||||
transparent?: boolean,
|
||||
/// Whether the window is maximized or not.
|
||||
maximized?: boolean,
|
||||
/// Whether the window is visible or not.
|
||||
visible?: boolean,
|
||||
/// Whether the window should have borders and bars.
|
||||
decorations?: boolean,
|
||||
/// Whether the window should always be on top of other windows.
|
||||
always_on_top?: boolean,
|
||||
/// Whether or not the window icon should be added to the taskbar.
|
||||
skip_taskbar?: boolean,
|
||||
}
|
||||
|
||||
interface BuildConfig {
|
||||
/// The binary used to build and run the application.
|
||||
runner?: string,
|
||||
/// The path or URL to use on development.
|
||||
devPath?: string,
|
||||
/// The path to the app's dist dir. This path must contain your index.html file.
|
||||
distDir?: string,
|
||||
/// A shell command to run before `tauri dev` kicks in.
|
||||
///
|
||||
/// The TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG environment variables are set if you perform conditional compilation.
|
||||
beforeDevCommand?: string,
|
||||
/// A shell command to run before `tauri build` kicks in.
|
||||
///
|
||||
/// The TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG environment variables are set if you perform conditional compilation.
|
||||
beforeBuildCommand?: string,
|
||||
/// Features passed to `cargo` commands.
|
||||
features?: string[],
|
||||
/// Whether we should inject the Tauri API on `window.__TAURI__` or not.
|
||||
withGlobalTauri?: boolean,
|
||||
}
|
2173
misc/config_tools/configurator/src-tauri/types/schema.json
Normal file
37
misc/config_tools/configurator/src/ACRNContext.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, {createContext} from 'react'
|
||||
import {Helper, TauriLocalFSBackend} from "./lib/helper";
|
||||
import {Configurator} from "./lib/acrn";
|
||||
|
||||
// 1. Use React createContext API to create ACRN Context
|
||||
export const ACRNContext = createContext({
|
||||
helper: () => {
|
||||
},
|
||||
configurator: () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Create Context Provider
|
||||
export class ACRNProvider extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let fsBackend = new TauriLocalFSBackend()
|
||||
let helper = new Helper(fsBackend, fsBackend)
|
||||
let configurator = new Configurator(helper)
|
||||
this.state = {
|
||||
helper: helper,
|
||||
configurator: configurator
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.state)
|
||||
return (
|
||||
<ACRNContext.Provider value={this.state}>
|
||||
{this.props.children}
|
||||
</ACRNContext.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. export Consumer
|
||||
export const ACRNConsumer = ACRNContext.Consumer
|
97
misc/config_tools/configurator/src/App.css
Normal file
@ -0,0 +1,97 @@
|
||||
a {
|
||||
/* Browse for folder… */
|
||||
font-family: Roboto, serif;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
|
||||
color: #2A958D;
|
||||
text-decoration: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.border-end-sm {
|
||||
border-right: 1px solid #dee2e6 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
min-height: 50px;
|
||||
border-radius: 30px;
|
||||
font-family: Roboto, serif;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007B81;
|
||||
border-color: #007B81;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #004B50;
|
||||
border-color: #004B50;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 49px;
|
||||
background-image: url("assets/images/dropdown_arrow.svg") !important;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 16px 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border: 1px solid #69BFAD;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 6px rgba(105, 191, 173, 0.37);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
.banner-text {
|
||||
max-width: 594px;
|
||||
|
||||
font-family: Roboto, serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 28px;
|
||||
line-height: 37px;
|
||||
letter-spacing: -0.175px;
|
||||
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bg-gray {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.descInfoBtn {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.descInfoBtn:hover {
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
.rst-paragraph:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
25
misc/config_tools/configurator/src/App.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import {ACRNProvider} from "./ACRNContext";
|
||||
import {HashRouter, Routes, Route} from "react-router-dom";
|
||||
import './App.css'
|
||||
import Welcome from "./pages/Welcome/Welcome";
|
||||
import Error from "./pages/Error/Error";
|
||||
import Config from "./pages/Config/Config";
|
||||
|
||||
import "bootstrap/dist/js/bootstrap"
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ACRNProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Welcome/>}/>
|
||||
<Route path="/config" element={<Config/>}/>
|
||||
<Route path="*" element={<Error/>}/>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ACRNProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default App
|
@ -0,0 +1,9 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('Roboto-Regular.woff2') format('woff2'),
|
||||
url('Roboto-Regular.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg width="11" height="19" viewBox="0 0 11 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.7425 19C6.973 19 6.2225 18.6372 5.7665 17.9674L0 9.50154L5.757 1.03572C6.4885 -0.0341323 7.961 -0.322528 9.0535 0.39381C10.146 1.11015 10.4405 2.55213 9.709 3.62198L5.7 9.50154L9.709 15.3811C10.4405 16.451 10.146 17.8929 9.0535 18.6093C8.6545 18.8698 8.189 19 7.7425 19Z" fill="#69BFAD"/>
|
||||
</svg>
|
After Width: | Height: | Size: 404 B |
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="9" viewBox="0 0 18 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 8.5947L0.75205 3.09606C-0.0247838 2.57592 -0.234191 1.52888 0.28595 0.75205C0.806091 -0.0247838 1.85313 -0.234191 2.62996 0.28595L9 4.53489L15.37 0.28595C16.1469 -0.234191 17.1939 -0.0247838 17.7141 0.75205C18.2342 1.52888 18.0248 2.57592 17.248 3.09606L9 8.5947Z" fill="#69BFAD"/>
|
||||
</svg>
|
After Width: | Height: | Size: 399 B |
@ -0,0 +1,78 @@
|
||||
"""
|
||||
|
||||
public var board_xml
|
||||
public function get_enum(source, selector)
|
||||
|
||||
enum = {
|
||||
'type': 'dynamicEnum',
|
||||
'function': 'get_enum',
|
||||
'source': 'board_xml',
|
||||
'selector': element['xs:annotation']['@acrn:options'],
|
||||
'sorted': element['xs:annotation'].get('@acrn:options-sorted-by', None)
|
||||
}
|
||||
|
||||
params defined before this document string in js side like
|
||||
```js
|
||||
let params = JSON.stringify({board_xml: boardXMLText, scenario_json: JSON.stringify(scenario_json)})
|
||||
params = Base64.encode(params);
|
||||
return runPyCode(`
|
||||
params="${params}"
|
||||
${dynamicScenario}
|
||||
`)
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
from base64 import b64decode
|
||||
|
||||
import elementpath
|
||||
import lxml.etree as etree
|
||||
|
||||
# local test var set
|
||||
if 'params' not in globals():
|
||||
# params = b64encode(open(r"board.xml",'rb').read())
|
||||
raise NotImplementedError
|
||||
|
||||
# Main flow
|
||||
# noinspection PyUnresolvedReferences,PyUnboundLocalVariable
|
||||
params: str = b64decode(params).decode('utf-8')
|
||||
params: dict = json.loads(params)
|
||||
board_xml = etree.XML(params["board_xml"])
|
||||
scenario_json = params['scenario_json']
|
||||
|
||||
|
||||
def get_enum(source, options):
|
||||
elements = [str(x) for x in elementpath.select(source, options) if x]
|
||||
elements = list(set(elements))
|
||||
if not elements:
|
||||
elements = ['']
|
||||
return elements
|
||||
|
||||
|
||||
def dynamic_enum(**kwargs):
|
||||
# value from env
|
||||
function, source = [globals()[kwargs[key]] for key in ['function', 'source']]
|
||||
# value from given
|
||||
selector, sorted_func = [kwargs[key] for key in ['selector', 'sorted']]
|
||||
|
||||
# get enum data
|
||||
enum = function(source, selector)
|
||||
if sorted_func:
|
||||
enum = sorted(enum, key=eval(sorted_func))
|
||||
return enum
|
||||
|
||||
|
||||
def dynamic_enum_apply(obj):
|
||||
# get json schema enum obj
|
||||
if 'enum' in obj and isinstance(obj['enum'], dict):
|
||||
enum_setting = obj['enum']
|
||||
# check enum obj type
|
||||
if enum_setting['type'] == 'dynamicEnum':
|
||||
del enum_setting['type']
|
||||
# replace json schema obj enum field data
|
||||
obj['enum'] = dynamic_enum(**enum_setting)
|
||||
return obj
|
||||
|
||||
|
||||
data = json.loads(scenario_json, object_hook=dynamic_enum_apply)
|
||||
json.dumps(data)
|
2392
misc/config_tools/configurator/src/assets/schema/scenario.json
Normal file
@ -0,0 +1,6 @@
|
||||
.banner {
|
||||
min-height: 50px;
|
||||
background-image: url("images/top_pattern.png"), linear-gradient(90.29deg, #242357 0.08%, #6ABFAE 85.19%, #69BFAD 99.72%);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export class Banner extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="banner">{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,5 @@
|
||||
import './Banner.css'
|
||||
import {Banner} from "./Banner";
|
||||
|
||||
|
||||
export default Banner;
|
@ -0,0 +1,45 @@
|
||||
import {Button, Modal} from "react-bootstrap";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function Confirm(props) {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const handleClose = (choice) => {
|
||||
setShow(false);
|
||||
props.callback(choice)
|
||||
}
|
||||
|
||||
const handleShow = (e) => {
|
||||
setShow(true);
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal show={show} onHide={() => handleClose('cancel')} size="lg">
|
||||
<Modal.Header closeButton closeVariant="white" className="bg-primary text-white">
|
||||
<Modal.Title>{props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="p-4">
|
||||
<b>{props.content}</b>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="px-5 py-4 my-2">
|
||||
<Button className="me-sm-2" variant="outline-primary" onClick={() => handleShow('cancel')}
|
||||
size="lg" style={{width: '137px'}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="me-sm-2" variant="outline-primary" onClick={() => handleShow('no')} size="lg"
|
||||
style={{width: '137px'}}>
|
||||
{props.no ? props.no : "No"}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => {
|
||||
props.callback('yes')
|
||||
}} size="lg" style={{width: '137px'}}>
|
||||
{props.yes ? props.yes : "Yes"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
import Confirm from "./Confirm";
|
||||
|
||||
export default Confirm
|
@ -0,0 +1,25 @@
|
||||
import {Component} from "react";
|
||||
import {getVersion} from "@tauri-apps/api/app";
|
||||
|
||||
|
||||
export default class Footer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
version: "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
getVersion().then((version) => {
|
||||
this.setState({version: version})
|
||||
})
|
||||
}
|
||||
|
||||
render = () => {
|
||||
return <div className="pt-3">
|
||||
<p className="text-center text-secondary">© Copyright Project ACRN™, a Series of LF Projects, LLC.
|
||||
- Version {this.state.version}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import Footer from "./footer";
|
||||
|
||||
export default Footer
|
@ -0,0 +1,44 @@
|
||||
body {
|
||||
-webkit-app-region: no-drag
|
||||
}
|
||||
|
||||
.bg-navbar {
|
||||
height: 80px;
|
||||
background: #007B81;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
cursor: default;
|
||||
font-family: Roboto, serif;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-size: 26px;
|
||||
line-height: 30px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
|
||||
color: #9ADFD1;
|
||||
}
|
||||
|
||||
.controlButtons {
|
||||
margin-right: 30px;
|
||||
width: 130px;
|
||||
filter: drop-shadow(3px 3px 2px rgb(0 0 0 / 0.4));
|
||||
}
|
||||
|
||||
|
||||
.wmb {
|
||||
cursor: pointer;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.wmb:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import {Container} from "react-bootstrap";
|
||||
import {Navbar as BootstrapNavbar} from "react-bootstrap";
|
||||
import logo from "./images/ACRN_Logo.svg";
|
||||
import {faWindowMaximize} from "@fortawesome/free-regular-svg-icons";
|
||||
import {faClose, faMinus} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {windowHelper} from "../../lib/platform/tauri/tauri";
|
||||
|
||||
|
||||
export class Navbar extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<BootstrapNavbar bg="navbar" variant="dark" height="80">
|
||||
<Container fluid data-tauri-drag-region>
|
||||
<BootstrapNavbar.Brand>
|
||||
<img
|
||||
alt="ACRN" src={logo} height="38" data-tauri-drag-region="true"
|
||||
className="d-inline-block align-self-center mx-3"
|
||||
/>
|
||||
<div className="d-inline align-bottom logo-text" data-tauri-drag-region="true">
|
||||
Configurator
|
||||
</div>
|
||||
</BootstrapNavbar.Brand>
|
||||
<div className="controlButtons d-flex justify-content-between align-items-center"
|
||||
data-tauri-drag-region={true}>
|
||||
<FontAwesomeIcon className="wmb" icon={faMinus} color="white" size="lg"
|
||||
onClick={windowHelper.minimal}/>
|
||||
<FontAwesomeIcon className="wmb" icon={faWindowMaximize} color="white" size="lg"
|
||||
onClick={windowHelper.maxmal}/>
|
||||
<FontAwesomeIcon className="wmb" icon={faClose} color="white" size="lg"
|
||||
onClick={windowHelper.close}/>
|
||||
</div>
|
||||
</Container>
|
||||
</BootstrapNavbar>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<svg width="150" height="38" viewBox="0 0 150 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_153_432)">
|
||||
<path d="M49.4131 6.45779L36.9832 31.2792H39.7601L42.9943 24.8355C44.5379 22.3463 47.6728 20.8086 50.6057 20.8086C53.6503 20.8086 56.9164 22.4522 58.4068 25.0985H58.4085L61.5116 31.2809H64.2885L51.8604 6.45956H49.4131V6.45779ZM50.6057 18.3247C48.8655 18.3247 47.1766 18.7449 45.6685 19.5076L50.6359 9.61078L55.6209 19.5464C54.0951 18.7573 52.3814 18.3247 50.6057 18.3247Z" fill="white"/>
|
||||
<path d="M117.389 16.773C117.389 10.8907 112.938 6.45602 107.033 6.45602H96.6782V31.2774H99.1699V8.93816H107.033C111.369 8.93816 114.898 12.4531 114.898 16.773C114.898 19.5058 113.503 21.9985 111.166 23.4391L110.583 23.7992L118.019 31.2792H121.517L114.421 24.1505C115.286 23.1354 117.389 20.3126 117.389 16.773Z" fill="white"/>
|
||||
<path d="M147.133 6.45779V26.9751L130.367 6.45779H127.512V31.2792H130.004V9.92502L147.419 31.2792H149.625V6.45779H147.133Z" fill="white"/>
|
||||
<path d="M87.1884 25.8012C85.294 27.8896 82.5347 29.0866 79.616 29.0866C74.0355 29.0866 69.497 24.5018 69.497 18.8685C69.497 13.2351 74.0372 8.65041 79.616 8.65041C82.5276 8.65041 85.2833 9.84382 87.1778 11.9234L87.5269 12.3065L89.4231 10.7548L89.0385 10.3328C86.5522 7.60177 83.3623 6.15768 79.8145 6.15768C72.779 6.15768 67.0549 11.8599 67.0549 18.8685C67.0549 25.8771 72.779 31.5793 79.8145 31.5793C83.3712 31.5793 86.5646 30.1299 89.0527 27.3883L89.4373 26.9663L87.5393 25.4181L87.1884 25.8012Z" fill="white"/>
|
||||
<path d="M15.2955 15.3836C9.30207 15.3836 5.05067 17.0501 4.87345 17.1207L4.4375 17.2938V30.7884L15.2955 38L26.1535 30.7884V17.2955L25.7176 17.1207C25.5386 17.0501 21.2889 15.3836 15.2955 15.3836ZM9.14258 28.827L12.9474 34.7817L6.42586 30.4512L9.14258 28.827ZM8.49574 27.6036L5.82333 29.2013V21.2147L8.49574 27.6036ZM11.8239 21.7037L19.1393 27.1976H9.82663L8.47802 23.974L11.8239 21.7037ZM7.93574 22.6729L6.05194 18.1712C7.50865 17.698 10.9325 16.7641 15.2955 16.7641C15.7226 16.7641 16.1568 16.773 16.5945 16.7924L7.93574 22.6729ZM13.0325 20.8828L18.2249 17.3573L20.1565 26.2337L13.0325 20.8828ZM19.9616 28.5781L15.2937 35.8833L10.6259 28.5781H19.9616ZM21.4343 25.5911L19.5788 17.0625C21.8596 17.3785 23.591 17.864 24.5373 18.1712L21.4343 25.5911ZM24.1634 30.4512L17.6418 34.7817L21.4467 28.827L24.1634 30.4512ZM22.0935 27.6036L24.7659 21.2147V29.2013L22.0935 27.6036Z" fill="white"/>
|
||||
<path d="M26.3343 3.56079L19.7117 2.73635L18.7973 0H11.7937L10.8775 2.73635L4.25495 3.56079L0 6.74026V11.3356L4.42862 15.7473L5.15698 15.4577C5.19951 15.4419 9.45269 13.7859 15.2955 13.7859C21.1436 13.7859 25.3915 15.4419 25.4322 15.4577L26.1606 15.7473L30.5892 11.3356V6.74026L26.3343 3.56079ZM28.7178 11.2296L25.8221 14.1143C25.5687 14.0207 24.9573 13.8053 24.0464 13.5547L26.8996 10.0203L28.7178 11.2296ZM29.191 7.43582V9.86853L27.3851 8.66983L26.7755 5.63159L29.191 7.43582ZM25.4056 5.91758L26.0064 8.90462L23.1922 12.3895L20.9451 9.25064L25.4056 5.91758ZM19.8251 10.0874L21.9535 13.0603C20.2256 12.7108 18.4623 12.4972 16.699 12.423L19.8251 10.0874ZM16.5838 6.08353L19.6373 4.13101L24.6348 4.75243L19.9545 8.24966L16.5838 6.08353ZM15.2902 5.25203L12.1783 3.25361L12.8003 1.39289H17.7871L18.4092 3.25361L15.2902 5.25203ZM15.2884 6.91326L18.7601 9.14118L15.2955 11.731L11.8185 9.13236L15.2884 6.91326ZM13.8902 12.423C12.1233 12.4972 10.36 12.7126 8.62863 13.0621L10.7481 10.0768L13.8902 12.423ZM5.95622 4.75419L10.9572 4.13101L13.9947 6.08177L10.6241 8.24084L5.95622 4.75419ZM9.6299 9.24005L7.39521 12.3877L4.58457 8.90639L5.18356 5.91935L9.6299 9.24005ZM3.68963 10.0221L6.5428 13.5564C5.63014 13.8071 5.01875 14.0225 4.7671 14.1161L1.8714 11.2296L3.68963 10.0221ZM3.81545 5.63159L3.20583 8.66983L1.39823 9.86853V7.43582L3.81545 5.63159Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_153_432">
|
||||
<rect width="149.625" height="38" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,5 @@
|
||||
import './Navbar.css'
|
||||
import {Navbar} from "./Navbar";
|
||||
|
||||
|
||||
export default Navbar;
|
15
misc/config_tools/configurator/src/favicon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
13
misc/config_tools/configurator/src/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
36
misc/config_tools/configurator/src/index.scss
Normal file
@ -0,0 +1,36 @@
|
||||
// global
|
||||
$primary: #007B81;
|
||||
|
||||
// grid padding
|
||||
$grid-gutter-width: 0;
|
||||
|
||||
// a
|
||||
$link-decoration: auto;
|
||||
$link-shade-percentage: 39%;
|
||||
|
||||
// text box fontsize
|
||||
$input-font-size-lg: 1rem;
|
||||
|
||||
@import "../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.text-pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.btn.btn-outline-primary {
|
||||
color: #007B81;
|
||||
border-color: #007B81;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
color: #004B50;
|
||||
border-color: #004B50;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.5195 8.99331L17.4644 3.04835C18.1607 2.3521 18.1607 1.21845 17.4644 0.522192C16.7682 -0.174064 15.6345 -0.174064 14.9383 0.522192L8.99331 6.46715L3.04835 0.531118C2.3521 -0.165138 1.21845 -0.165138 0.522192 0.531118C-0.174064 1.22737 -0.174064 2.36102 0.522192 3.05728L6.46715 9.00223L0.522192 14.9472C-0.174064 15.6434 -0.174064 16.7771 0.522192 17.4733C0.87032 17.8215 1.32556 18 1.78081 18C2.23605 18 2.6913 17.8215 3.03942 17.4733L8.98438 11.5284L14.9293 17.4733C15.2775 17.8215 15.7327 18 16.1879 18C16.6432 18 17.0984 17.8215 17.4466 17.4733C18.1428 16.7771 18.1428 15.6434 17.4466 14.9472L11.5195 8.99331Z' fill='white'/%3E%3C/svg%3E%0A");
|
||||
}
|
2
misc/config_tools/configurator/src/lib/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
pyodide
|
||||
/pyodide.tar.bz2
|
476
misc/config_tools/configurator/src/lib/acrn.jsx
Normal file
@ -0,0 +1,476 @@
|
||||
import _ from "lodash";
|
||||
|
||||
import React, {Component} from "react";
|
||||
import {path} from "@tauri-apps/api";
|
||||
import scenario from '../assets/schema/scenario.json'
|
||||
import {createSearchParams} from "react-router-dom";
|
||||
import queryString from 'query-string'
|
||||
import {invoke} from "@tauri-apps/api/tauri";
|
||||
import {getNewSchema} from "./runpy";
|
||||
|
||||
|
||||
function ThrowError(errMsg) {
|
||||
alert(errMsg)
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
function NameTrans(name) {
|
||||
return {
|
||||
"recentlyWorkingFolders": "WorkingFolder",
|
||||
"board": "Board",
|
||||
"scenario": "Scenario",
|
||||
}[name.replace("History", "")]
|
||||
}
|
||||
|
||||
class EventBase extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.funRegister = {}
|
||||
this.funRegisterID = 0
|
||||
this.eventName = {
|
||||
scenarioDataUpdate: 'scenarioDataUpdate'
|
||||
}
|
||||
}
|
||||
|
||||
register = (eventName, fun) => {
|
||||
if (this.funRegister.hasOwnProperty(eventName)) {
|
||||
this.funRegisterID++
|
||||
this.funRegister[eventName][this.funRegisterID] = fun
|
||||
return this.funRegisterID
|
||||
}
|
||||
}
|
||||
|
||||
unregister = (eventName, funRegisterID) => {
|
||||
if (this.funRegister.hasOwnProperty(eventName)) {
|
||||
if (this.funRegister[eventName].hasOwnProperty(funRegisterID)) {
|
||||
delete this.funRegister[eventName][funRegisterID]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class XMLLayer extends EventBase {
|
||||
constructor(helper) {
|
||||
super();
|
||||
this.helper = helper
|
||||
this.funRegister = {
|
||||
onScenarioLoad: []
|
||||
}
|
||||
}
|
||||
|
||||
#parseXML = (XMLText) => {
|
||||
return (new DOMParser()).parseFromString(XMLText, "text/xml")
|
||||
}
|
||||
|
||||
#validateBoardXMLText = boardXMLText => {
|
||||
// call inside
|
||||
try {
|
||||
let boardXML = this.#parseXML(boardXMLText, "text/xml");
|
||||
return !!boardXML.querySelector("BIOS_INFO")
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
#validateScenarioXMLText = (scenarioXMLText) => {
|
||||
// call inside
|
||||
try {
|
||||
let scenarioXML = this.#parseXML(scenarioXMLText);
|
||||
return !!scenarioXML.querySelector("acrn-config")
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
loadBoard = async (boardXMLPath) => {
|
||||
// call by program
|
||||
let boardXMLText = await this.helper.read(await this.helper.resolveHome(boardXMLPath))
|
||||
if (this.#validateBoardXMLText(boardXMLText)) {
|
||||
let PCIDevices = this.getPCIDevice(boardXMLText)
|
||||
return {boardXMLText, PCIDevices}
|
||||
} else {
|
||||
alert('Board XML Error!')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
loadScenario = async (scenarioXMLPath) => {
|
||||
// call by program
|
||||
// load scenario data from xml file
|
||||
let scenarioXMLText = await this.helper.read(await this.helper.resolveHome(scenarioXMLPath))
|
||||
if (!this.#validateScenarioXMLText(scenarioXMLText)) {
|
||||
ThrowError('Scenario XML Error!')
|
||||
}
|
||||
|
||||
return this.helper.convertXMLTextToObj(scenarioXMLText)['acrn-config']
|
||||
}
|
||||
|
||||
getPCIDevice = (boardXMLText) => {
|
||||
let pci_devices = this.#parseXML(boardXMLText).querySelector("PCI_DEVICE").textContent
|
||||
// Remove Region line
|
||||
pci_devices = pci_devices.replace(/Region.+\s+/g, '\n')
|
||||
// Remove Space
|
||||
pci_devices = pci_devices.replace(/[\n\t]+/g, '\n')
|
||||
// Split by \n
|
||||
pci_devices = pci_devices.split('\n')
|
||||
|
||||
return _.uniq(pci_devices)
|
||||
}
|
||||
|
||||
saveBoard = (boardFileWritePath, boardData) => {
|
||||
this.helper.save(boardFileWritePath, boardData)
|
||||
}
|
||||
|
||||
saveScenario = (scenarioWritePath, scenarioData) => {
|
||||
// call by program
|
||||
console.log(scenarioData)
|
||||
const scenarioXML = this.helper.convertObjToXML({'acrn-config': scenarioData})
|
||||
console.log(scenarioXML)
|
||||
// debugger
|
||||
this.helper.save(scenarioWritePath, scenarioXML)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ProgramLayer extends EventBase {
|
||||
constructor(helper, instanceOfXMLLayer) {
|
||||
super()
|
||||
this.helper = helper
|
||||
this.vmID = 0
|
||||
this.scenarioData = {}
|
||||
this.initScenario()
|
||||
this.xmlLayer = instanceOfXMLLayer
|
||||
this.funRegister = {
|
||||
scenarioDataUpdate: []
|
||||
}
|
||||
}
|
||||
|
||||
initScenario = () => {
|
||||
this.vmID = 0
|
||||
this.scenarioData = {
|
||||
hv: {},
|
||||
vm: {
|
||||
PRE_LAUNCHED_VM: [],
|
||||
SERVICE_VM: [],
|
||||
POST_LAUNCHED_VM: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onScenarioDataUpdateEvent() {
|
||||
this.funRegister.scenarioDataUpdate.map((f) => f())
|
||||
}
|
||||
|
||||
newScenario = (preLaunchedVM, serviceVM, postLaunchedVM) => {
|
||||
// call by view
|
||||
this.initScenario()
|
||||
for (let i = 0; i < preLaunchedVM; i++) {
|
||||
this.addVM('PRE_LAUNCHED_VM')
|
||||
}
|
||||
for (let i = 0; i < serviceVM; i++) {
|
||||
this.addVM('SERVICE_VM')
|
||||
}
|
||||
for (let i = 0; i < postLaunchedVM; i++) {
|
||||
this.addVM('POST_LAUNCHED_VM')
|
||||
}
|
||||
this.onScenarioDataUpdateEvent()
|
||||
}
|
||||
|
||||
|
||||
addVM = (VMType, vmData = {}) => {
|
||||
// call by inside and view
|
||||
// if provide VMType and vmData at same time
|
||||
// function will set VMType to vmData
|
||||
let vm = vmData
|
||||
let usedVMID = false;
|
||||
|
||||
// check VMType
|
||||
let vmType = VMType;
|
||||
if (!_.isString(vmType) || _.isEmpty(vmType)) {
|
||||
// vmType is empty or not correct
|
||||
if (vm.hasOwnProperty('load_order')) {
|
||||
vmType = vm.load_order;
|
||||
} else {
|
||||
ThrowError('No VMType Set')
|
||||
}
|
||||
} else {
|
||||
if (vm.hasOwnProperty('load_order') && vm.load_order !== vmType) {
|
||||
console.warn("vmType and vmData provide load_order at same time, set vmData's load_order to vmType now")
|
||||
}
|
||||
vm.load_order = vmType
|
||||
}
|
||||
|
||||
|
||||
// check and validate vm name
|
||||
if (!vm.hasOwnProperty('name') || !_.isString(vm.name) || _.isEmpty(vm.name)) {
|
||||
vm.name = 'VM' + this.vmID
|
||||
usedVMID = true
|
||||
}
|
||||
|
||||
vm['@id'] = this.vmID
|
||||
this.vmID++
|
||||
|
||||
// add to config
|
||||
this.scenarioData.vm[vmType].push(vm)
|
||||
this.onScenarioDataUpdateEvent()
|
||||
}
|
||||
|
||||
deleteVM = (vmID) => {
|
||||
// call by view
|
||||
for (let vmType in this.scenarioData.vm) {
|
||||
this.scenarioData.vm[vmType].map((vmConfig, vmIndex) => {
|
||||
if (vmConfig['@id'] === vmID) {
|
||||
this.scenarioData.vm[vmType].splice(vmIndex, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.onScenarioDataUpdateEvent()
|
||||
}
|
||||
|
||||
loadBoard = async (WorkingFolder, boardXMLPath) => {
|
||||
// call by view
|
||||
let boardData = await this.xmlLayer.loadBoard(boardXMLPath)
|
||||
if (boardData === false) {
|
||||
return false
|
||||
}
|
||||
let {boardXMLText, PCIDevices} = boardData
|
||||
|
||||
// get new board file name
|
||||
let newBoardFileName = await path.basename(boardXMLPath)
|
||||
let cut = 0
|
||||
if (_.endsWith(newBoardFileName.toLowerCase(), '.xml')) {
|
||||
cut = '.xml'.length
|
||||
}
|
||||
if (_.endsWith(newBoardFileName.toLowerCase(), '.board.xml')) {
|
||||
cut = '.board.xml'.length
|
||||
}
|
||||
newBoardFileName = newBoardFileName.slice(0, newBoardFileName.length - cut)
|
||||
newBoardFileName = newBoardFileName + '.board.xml'
|
||||
|
||||
// new board file save path
|
||||
const boardFileWritePath = await path.join(await this.helper.resolveHome(WorkingFolder), newBoardFileName)
|
||||
|
||||
// remove current working folder old Board File first
|
||||
await this.removeOldBoardFile(WorkingFolder)
|
||||
|
||||
// save board file to working director
|
||||
this.xmlLayer.saveBoard(boardFileWritePath, boardXMLText)
|
||||
// get shownName
|
||||
let shownName = await path.join(WorkingFolder, newBoardFileName)
|
||||
console.log({shownName, boardXMLText, PCIDevices})
|
||||
return {shownName, boardXMLText, PCIDevices}
|
||||
}
|
||||
|
||||
loadScenario = async (scenarioXMLPath) => {
|
||||
// call by view
|
||||
let scenarioConfig = await this.xmlLayer.loadScenario(scenarioXMLPath)
|
||||
this.initScenario()
|
||||
this.scenarioData.hv = scenarioConfig.hv;
|
||||
scenarioConfig.vm.map((vmConfig, index) => {
|
||||
let vmType = vmConfig.load_order
|
||||
if (!this.scenarioData.vm.hasOwnProperty(vmType)) {
|
||||
try {
|
||||
ThrowError('VM @id=' + index + ' VMType Does Not Exist')
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} else {
|
||||
//fix pci_devs is object issue
|
||||
if (vmConfig.hasOwnProperty("pci_devs") && vmConfig.pci_devs.hasOwnProperty("pci_dev") && _.isString(vmConfig.pci_devs.pci_dev)) {
|
||||
vmConfig.pci_devs.pci_dev = [vmConfig.pci_devs.pci_dev]
|
||||
}
|
||||
this.addVM(vmType, vmConfig)
|
||||
}
|
||||
})
|
||||
this.onScenarioDataUpdateEvent()
|
||||
}
|
||||
|
||||
getOriginScenarioData = () => {
|
||||
// call by inside
|
||||
let originScenario = _.cloneDeep(this.scenarioData)
|
||||
originScenario.vm = originScenario.vm.PRE_LAUNCHED_VM.concat(
|
||||
originScenario.vm.SERVICE_VM,
|
||||
originScenario.vm.POST_LAUNCHED_VM
|
||||
)
|
||||
return originScenario
|
||||
}
|
||||
|
||||
async removeOldBoardFile(WorkingFolder) {
|
||||
let files = await this.helper.list(await this.helper.resolveHome(WorkingFolder))
|
||||
files.map((filename) => {
|
||||
if (_.endsWith(filename, '.board.xml')) {
|
||||
this.helper.remove(filename)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
saveScenario = async (WorkingFolder) => {
|
||||
// call by view
|
||||
let originScenarioData = this.getOriginScenarioData()
|
||||
let filename = 'scenario.xml'
|
||||
let scenarioWritePath = await path.join(await this.helper.resolveHome(WorkingFolder), filename)
|
||||
this.xmlLayer.saveScenario(scenarioWritePath, originScenarioData)
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
let shownPath = await path.join(WorkingFolder, filename)
|
||||
return shownPath
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Configurator extends EventBase {
|
||||
// get data from Program
|
||||
// convert it to view data
|
||||
constructor(helper) {
|
||||
super()
|
||||
|
||||
this.WorkingFolder = this.#getURLParam('WorkingFolder')
|
||||
|
||||
this.helper = helper
|
||||
this.XMLLayer = new XMLLayer(this.helper)
|
||||
this.programLayer = new ProgramLayer(this.helper, this.XMLLayer)
|
||||
|
||||
this.vmSchemas = this.Schemas()
|
||||
this.hvSchema = this.vmSchemas.HV
|
||||
delete this.vmSchemas.HV
|
||||
|
||||
this.updateSchema()
|
||||
}
|
||||
|
||||
#getURLParam(key) {
|
||||
let hash = location.hash
|
||||
let params = hash.substring(hash.indexOf('?'))
|
||||
params = queryString.parse(params)
|
||||
return params[key]
|
||||
}
|
||||
|
||||
#buildPageParams(url, queryParams = {}) {
|
||||
let data = {pathname: url}
|
||||
if (queryParams) {
|
||||
data.search = createSearchParams(queryParams).toString()
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
settingWorkingFolder = (WorkingFolder) => {
|
||||
this.WorkingFolder = WorkingFolder
|
||||
return this.#buildPageParams('./config', {WorkingFolder})
|
||||
}
|
||||
|
||||
updateSchema = () => {
|
||||
let listingFunctions = [this.ivshmemEnum]
|
||||
listingFunctions.forEach((func) => {
|
||||
func()
|
||||
this.programLayer.register("scenarioDataUpdate", func)
|
||||
})
|
||||
}
|
||||
|
||||
ivshmemEnum = () => {
|
||||
let odata = this.programLayer.getOriginScenarioData()
|
||||
let vmNames = odata.vm.map((vmData) => {
|
||||
return vmData.name
|
||||
})
|
||||
if (vmNames.length === 0) {
|
||||
vmNames = ['']
|
||||
}
|
||||
this.hvSchema.basic.definitions.VMNameType.enum = vmNames
|
||||
}
|
||||
|
||||
|
||||
loadBoard = async (boardXMLPath, callback) => {
|
||||
let boardData = await this.programLayer.loadBoard(this.WorkingFolder, boardXMLPath)
|
||||
if (boardData === false) {
|
||||
await this.removeHistory('board', boardXMLPath)
|
||||
return
|
||||
}
|
||||
let {shownName, boardXMLText, PCIDevices} = boardData
|
||||
scenario.definitions.PCIDevsConfiguration.properties.pci_dev.items.enum = PCIDevices
|
||||
Object.keys(this.vmSchemas).map((VMTypeKey) => {
|
||||
Object.keys(this.vmSchemas[VMTypeKey]).map((configLevelKey) => {
|
||||
this.vmSchemas[VMTypeKey][configLevelKey].definitions.PCIDevsConfiguration.properties.pci_dev.items.enum = PCIDevices
|
||||
})
|
||||
})
|
||||
try {
|
||||
let new_scenario = getNewSchema(boardXMLText)
|
||||
scenario.definitions = new_scenario.definitions;
|
||||
Object.keys(this.vmSchemas).map((VMTypeKey) => {
|
||||
Object.keys(this.vmSchemas[VMTypeKey]).map((configLevelKey) => {
|
||||
this.vmSchemas[VMTypeKey][configLevelKey].definitions = new_scenario.definitions
|
||||
})
|
||||
})
|
||||
Object.keys(this.hvSchema).map((configLevelKey) => {
|
||||
this.hvSchema[configLevelKey].definitions = new_scenario.definitions;
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
callback(shownName, boardXMLText)
|
||||
}
|
||||
|
||||
async getHistory(key) {
|
||||
let p = await invoke("get_history", {historyType: NameTrans(key)})
|
||||
console.log("p", p);
|
||||
return JSON.parse(p)
|
||||
}
|
||||
|
||||
async addHistory(key, historyPath) {
|
||||
await invoke("add_history", {historyType: NameTrans(key), path: historyPath})
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async setHistory(key, history) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async removeHistory(key, historyPath) {
|
||||
let history = await this.getHistory(key)
|
||||
let index = history.indexOf(historyPath);
|
||||
if (index > -1) {
|
||||
history.splice(index, 1);
|
||||
}
|
||||
return this.setHistory(key, history)
|
||||
}
|
||||
|
||||
|
||||
saveScenario = async () => {
|
||||
let shownPath = await this.programLayer.saveScenario(this.WorkingFolder)
|
||||
return this.addHistory('scenario', shownPath)
|
||||
}
|
||||
|
||||
|
||||
static #getSchema(prefix) {
|
||||
let bo = scenario.definitions[prefix + "BasicConfigType"]
|
||||
let basic = _.cloneDeep(scenario);
|
||||
basic.type = bo.type;
|
||||
basic.required = bo.required;
|
||||
basic.properties = bo.properties;
|
||||
|
||||
let ao = scenario.definitions[prefix + "AdvancedConfigType"]
|
||||
let advanced = _.cloneDeep(scenario);
|
||||
advanced.type = ao.type;
|
||||
advanced.required = ao.required;
|
||||
advanced.properties = ao.properties;
|
||||
|
||||
return {basic, advanced}
|
||||
}
|
||||
|
||||
Schemas() {
|
||||
let prefixData = {
|
||||
HV: 'HV',
|
||||
PRE_LAUNCHED_VM: 'PreLaunchedVM',
|
||||
SERVICE_VM: 'ServiceVM',
|
||||
POST_LAUNCHED_VM: 'PostLaunchedVM'
|
||||
}
|
||||
for (let key in prefixData) {
|
||||
prefixData[key] = Configurator.#getSchema(prefixData[key])
|
||||
}
|
||||
return prefixData
|
||||
}
|
||||
|
||||
log() {
|
||||
this.helper.log(...arguments)
|
||||
}
|
||||
}
|
2
misc/config_tools/configurator/src/lib/bs4rjsf/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.js
|
||||
*.js.map
|
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import {AddButtonProps} from "@rjsf/core";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import {BsPlus} from "react-icons/bs";
|
||||
|
||||
const AddButton: React.FC<AddButtonProps> = props => (
|
||||
<Button {...props} style={{width: "100%"}} className={`ml-1 ${props.className}`}>
|
||||
<BsPlus/>
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default AddButton;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./AddButton";
|
||||
export * from "./AddButton";
|
@ -0,0 +1,210 @@
|
||||
import React from "react";
|
||||
import {utils} from "@rjsf/core";
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Container from "react-bootstrap/Container";
|
||||
import {ArrayFieldTemplateProps, IdSchema} from "@rjsf/core";
|
||||
|
||||
import AddButton from "../AddButton/AddButton";
|
||||
import IconButton from "../IconButton/IconButton";
|
||||
|
||||
const {isMultiSelect, getDefaultRegistry} = utils;
|
||||
|
||||
const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => {
|
||||
const {schema, registry = getDefaultRegistry()} = props;
|
||||
|
||||
if (isMultiSelect(schema, registry.rootSchema)) {
|
||||
return <DefaultFixedArrayFieldTemplate {...props} />;
|
||||
} else {
|
||||
return <DefaultNormalArrayFieldTemplate {...props} />;
|
||||
}
|
||||
};
|
||||
|
||||
type ArrayFieldTitleProps = {
|
||||
TitleField: any;
|
||||
idSchema: IdSchema;
|
||||
title: string;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
const ArrayFieldTitle = ({
|
||||
TitleField,
|
||||
idSchema,
|
||||
title,
|
||||
required,
|
||||
}: ArrayFieldTitleProps) => {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = `${idSchema.$id}__title`;
|
||||
return <TitleField id={id} title={title} required={required}/>;
|
||||
};
|
||||
|
||||
type ArrayFieldDescriptionProps = {
|
||||
DescriptionField: any;
|
||||
idSchema: IdSchema;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const ArrayFieldDescription = ({
|
||||
DescriptionField,
|
||||
idSchema,
|
||||
description,
|
||||
}: ArrayFieldDescriptionProps) => {
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = `${idSchema.$id}__description`;
|
||||
return <DescriptionField id={id} description={description}/>;
|
||||
};
|
||||
|
||||
// Used in the two templates
|
||||
const DefaultArrayItem = (props: any) => {
|
||||
const btnStyle = {
|
||||
flex: 1,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
fontWeight: "bold",
|
||||
};
|
||||
return (
|
||||
<div key={props.key}>
|
||||
<Row className="mb-2 d-flex align-items-baseline">
|
||||
<Col xs="9" lg="9">{props.children}</Col>
|
||||
|
||||
<Col xs="3" lg="3" className="px-3">
|
||||
{props.hasToolbar && (
|
||||
<div className="d-flex flex-row">
|
||||
{(props.hasMoveUp || props.hasMoveDown) && (
|
||||
<div className="m-0 p-0">
|
||||
<IconButton
|
||||
icon="arrow-up"
|
||||
className="array-item-move-up"
|
||||
tabIndex={-1}
|
||||
style={btnStyle as any}
|
||||
disabled={
|
||||
props.disabled || props.readonly || !props.hasMoveUp
|
||||
}
|
||||
onClick={props.onReorderClick(props.index, props.index - 1)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(props.hasMoveUp || props.hasMoveDown) && (
|
||||
<div className="m-0 p-0">
|
||||
<IconButton
|
||||
icon="arrow-down"
|
||||
tabIndex={-1}
|
||||
style={btnStyle as any}
|
||||
disabled={
|
||||
props.disabled || props.readonly || !props.hasMoveDown
|
||||
}
|
||||
onClick={props.onReorderClick(props.index, props.index + 1)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.hasRemove && (
|
||||
<div className="m-0 p-0">
|
||||
<IconButton
|
||||
icon="remove"
|
||||
tabIndex={-1}
|
||||
style={btnStyle as any}
|
||||
disabled={props.disabled || props.readonly}
|
||||
onClick={props.onDropIndexClick(props.index)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultFixedArrayFieldTemplate = (props: ArrayFieldTemplateProps) => {
|
||||
return (
|
||||
<fieldset className={props.className}>
|
||||
<ArrayFieldTitle
|
||||
key={`array-field-title-${props.idSchema.$id}`}
|
||||
TitleField={props.TitleField}
|
||||
idSchema={props.idSchema}
|
||||
title={props.uiSchema["ui:title"] || props.title}
|
||||
required={props.required}
|
||||
/>
|
||||
|
||||
{(props.uiSchema["ui:description"] || props.schema.description) && (
|
||||
<div
|
||||
className="field-description"
|
||||
key={`field-description-${props.idSchema.$id}`}>
|
||||
{props.uiSchema["ui:description"] || props.schema.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="row array-item-list"
|
||||
key={`array-item-list-${props.idSchema.$id}`}>
|
||||
{props.items && props.items.map(DefaultArrayItem)}
|
||||
</div>
|
||||
|
||||
{props.canAdd && (
|
||||
<AddButton
|
||||
className="array-item-add"
|
||||
onClick={props.onAddClick}
|
||||
disabled={props.disabled || props.readonly}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultNormalArrayFieldTemplate = (props: ArrayFieldTemplateProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Row className="p-0 m-0">
|
||||
<Col className="p-0 m-0">
|
||||
<ArrayFieldTitle
|
||||
key={`array-field-title-${props.idSchema.$id}`}
|
||||
TitleField={props.TitleField}
|
||||
idSchema={props.idSchema}
|
||||
title={props.uiSchema["ui:title"] || props.title}
|
||||
required={props.required}
|
||||
/>
|
||||
|
||||
{(props.uiSchema["ui:description"] || props.schema.description) && (
|
||||
<ArrayFieldDescription
|
||||
key={`array-field-description-${props.idSchema.$id}`}
|
||||
DescriptionField={props.DescriptionField}
|
||||
idSchema={props.idSchema}
|
||||
description={
|
||||
props.uiSchema["ui:description"] || props.schema.description
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Container fluid key={`array-item-list-${props.idSchema.$id}`} className="p-0 m-0">
|
||||
{props.items && props.items.map(p => DefaultArrayItem(p))}
|
||||
|
||||
{props.canAdd && (
|
||||
<Container className="">
|
||||
<Row className="mt-2">
|
||||
<Col xs={9}/>
|
||||
<Col xs={3} className="py-4 col-lg-3 col-3"> <AddButton
|
||||
className="array-item-add"
|
||||
onClick={props.onAddClick}
|
||||
disabled={props.disabled || props.readonly}
|
||||
/></Col>
|
||||
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</Container></Col>
|
||||
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArrayFieldTemplate;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./ArrayFieldTemplate";
|
||||
export * from "./ArrayFieldTemplate";
|
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
const CheckboxWidget = (props: WidgetProps) => {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
required,
|
||||
disabled,
|
||||
readonly,
|
||||
label,
|
||||
schema,
|
||||
autofocus,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
} = props;
|
||||
|
||||
const _onChange = ({
|
||||
target: {checked},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onChange(checked);
|
||||
const _onBlur = ({
|
||||
target: {checked},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onBlur(id, checked);
|
||||
const _onFocus = ({
|
||||
target: {checked},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, checked);
|
||||
|
||||
const desc = label || schema.description;
|
||||
return (
|
||||
<Form.Group className={`checkbox ${disabled || readonly ? "disabled" : ""}`}>
|
||||
<Form.Check
|
||||
id={id}
|
||||
label={desc}
|
||||
checked={typeof value === "undefined" ? false : value}
|
||||
required={required}
|
||||
disabled={disabled || readonly}
|
||||
autoFocus={autofocus}
|
||||
onChange={_onChange}
|
||||
type="checkbox"
|
||||
onBlur={_onBlur}
|
||||
onFocus={_onFocus}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./CheckboxWidget";
|
||||
export * from "./CheckboxWidget";
|
@ -0,0 +1,103 @@
|
||||
import React from "react";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const selectValue = (value: any, selected: any, all: any) => {
|
||||
const at = all.indexOf(value);
|
||||
const updated = selected.slice(0, at).concat(value, selected.slice(at));
|
||||
|
||||
// As inserting values at predefined index positions doesn't work with empty
|
||||
// arrays, we need to reorder the updated selection to match the initial order
|
||||
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
|
||||
};
|
||||
|
||||
const deselectValue = (value: any, selected: any) => {
|
||||
return selected.filter((v: any) => v !== value);
|
||||
};
|
||||
|
||||
const CheckboxesWidget = ({
|
||||
schema,
|
||||
label,
|
||||
id,
|
||||
disabled,
|
||||
options,
|
||||
value,
|
||||
autofocus,
|
||||
readonly,
|
||||
required,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
}: WidgetProps) => {
|
||||
const {enumOptions, enumDisabled, inline} = options;
|
||||
|
||||
const _onChange = (option: any) => ({
|
||||
target: {checked},
|
||||
}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const all = (enumOptions as any).map(({value}: any) => value);
|
||||
|
||||
if (checked) {
|
||||
onChange(selectValue(option.value, value, all));
|
||||
} else {
|
||||
onChange(deselectValue(option.value, value));
|
||||
}
|
||||
};
|
||||
|
||||
const _onBlur = ({target: {value}}: React.FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, value);
|
||||
const _onFocus = ({
|
||||
target: {value},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Label htmlFor={id}>{label || schema.title}</Form.Label>
|
||||
<Form.Group>
|
||||
{(enumOptions as any).map((option: any, index: number) => {
|
||||
const checked = value.indexOf(option.value) !== -1;
|
||||
const itemDisabled =
|
||||
enumDisabled && (enumDisabled as any).indexOf(option.value) != -1;
|
||||
|
||||
return inline ? (
|
||||
<Form key={index}>
|
||||
<Form.Check
|
||||
required={required}
|
||||
inline
|
||||
className="bg-transparent border-0"
|
||||
custom
|
||||
checked={checked}
|
||||
type={"checkbox"}
|
||||
id={`${id}_${index}`}
|
||||
label={option.label}
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={_onChange(option)}
|
||||
onBlur={_onBlur}
|
||||
onFocus={_onFocus}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
/>
|
||||
</Form>
|
||||
) : (
|
||||
<Form key={index}>
|
||||
<Form.Check
|
||||
custom
|
||||
required={required}
|
||||
checked={checked}
|
||||
className="bg-transparent border-0"
|
||||
type={"checkbox"}
|
||||
id={`${id}_${index}`}
|
||||
label={option.label}
|
||||
autoFocus={autofocus && index === 0}
|
||||
onChange={_onChange(option)}
|
||||
onBlur={_onBlur}
|
||||
onFocus={_onFocus}
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
})}
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxesWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./CheckboxesWidget";
|
||||
export * from "./CheckboxesWidget";
|
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const ColorWidget = (props: WidgetProps) => {
|
||||
const {registry} = props;
|
||||
const {TextWidget} = registry.widgets;
|
||||
return <TextWidget {...props} type="color"/>;
|
||||
};
|
||||
|
||||
export default ColorWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./ColorWidget";
|
||||
export * from "./ColorWidget";
|
@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import {utils, WidgetProps} from "@rjsf/core";
|
||||
|
||||
const {localToUTC, utcToLocal} = utils;
|
||||
|
||||
const DateTimeWidget = (props: WidgetProps) => {
|
||||
const {registry} = props;
|
||||
const {TextWidget} = registry.widgets;
|
||||
const value = utcToLocal(props.value);
|
||||
const onChange = (value: any) => {
|
||||
props.onChange(localToUTC(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextWidget
|
||||
{...props}
|
||||
type="datetime-local"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./DateTimeWidget";
|
||||
export * from "./DateTimeWidget";
|
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const DateWidget = (props: WidgetProps) => {
|
||||
const {registry} = props;
|
||||
const {TextWidget} = registry.widgets;
|
||||
return (
|
||||
<TextWidget
|
||||
{...props}
|
||||
type="date"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./DateWidget";
|
||||
export * from "./DateWidget";
|
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import {FieldProps} from "@rjsf/core";
|
||||
|
||||
export interface DescriptionFieldProps extends Partial<FieldProps> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const DescriptionField = ({description}: Partial<FieldProps>) => {
|
||||
if (description) {
|
||||
return <div>
|
||||
<div className="mb-3" dangerouslySetInnerHTML={{__html: description}}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DescriptionField;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./DescriptionField";
|
||||
export * from "./DescriptionField";
|
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const EmailWidget = (props: WidgetProps) => {
|
||||
const {registry} = props;
|
||||
const {TextWidget} = registry.widgets;
|
||||
return <TextWidget {...props} type="email"/>;
|
||||
};
|
||||
|
||||
export default EmailWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./EmailWidget";
|
||||
export * from "./EmailWidget";
|
@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import Card from "react-bootstrap/Card";
|
||||
import ListGroup from "react-bootstrap/ListGroup";
|
||||
|
||||
import {ErrorListProps} from "@rjsf/core";
|
||||
|
||||
const ErrorList = ({errors}: ErrorListProps) => (
|
||||
<Card border="danger" className="mb-4">
|
||||
<Card.Header className="alert-danger">Errors</Card.Header>
|
||||
<Card.Body className="p-0">
|
||||
<ListGroup>
|
||||
{errors.map((error, i: number) => {
|
||||
return (
|
||||
<ListGroup.Item key={i} className="border-0">
|
||||
<span>{error.stack}</span>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default ErrorList;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./ErrorList";
|
||||
export * from "./ErrorList";
|
@ -0,0 +1,171 @@
|
||||
import React from "react";
|
||||
|
||||
import {FieldTemplateProps} from "@rjsf/core";
|
||||
|
||||
import Form from "react-bootstrap/Form";
|
||||
import ListGroup from "react-bootstrap/ListGroup";
|
||||
|
||||
import WrapIfAdditional from "./WrapIfAdditional";
|
||||
|
||||
import {OverlayTrigger, Popover} from "react-bootstrap";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCircleInfo, faCircleExclamation} from "@fortawesome/free-solid-svg-icons";
|
||||
import _ from "lodash";
|
||||
|
||||
const FieldTemplate = (
|
||||
{
|
||||
id,
|
||||
children,
|
||||
displayLabel,
|
||||
rawErrors = [],
|
||||
rawHelp,
|
||||
rawDescription,
|
||||
classNames,
|
||||
disabled,
|
||||
label,
|
||||
onDropPropertyClick,
|
||||
onKeyChange,
|
||||
readonly,
|
||||
required,
|
||||
schema,
|
||||
uiSchema
|
||||
}: FieldTemplateProps) => {
|
||||
|
||||
let descLabel = (uiSchema.hasOwnProperty("ui:descLabel") && uiSchema["ui:descLabel"] === true)
|
||||
let descWithChildren
|
||||
let showLabel = _.endsWith(id, 'IVSHMEM_VM_0_VBDF') || _.endsWith(id, 'IVSHMEM_VM_0_VM_NAME') || (
|
||||
id.indexOf('vuart_connection') > 0 && (_.endsWith(id, 'vm_name') || _.endsWith(id, 'io_port'))
|
||||
)
|
||||
let dlva = uiSchema.hasOwnProperty("ui:descLabelAli") && uiSchema["ui:descLabelAli"] === 'V'
|
||||
if (displayLabel && rawDescription) {
|
||||
let desc
|
||||
const icon = rawErrors.length > 0 ? faCircleExclamation : faCircleInfo;
|
||||
if (descLabel) {
|
||||
if (dlva) {
|
||||
desc = <OverlayTrigger
|
||||
trigger={["hover", "focus"]}
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={
|
||||
<Popover id={`popover-positioned-top`}>
|
||||
<Popover.Body>
|
||||
<Form.Text className={rawErrors.length > 0 ? "text-danger" : "text-muted"}
|
||||
dangerouslySetInnerHTML={{__html: rawDescription}}/>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}>
|
||||
<div className="mx-2 py-2 row">
|
||||
<Form.Label
|
||||
className={(showLabel ? 'col-12 ps-4' : 'd-none') + " col-form-label " + (rawErrors.length > 0 ? "text-danger" : "")}>
|
||||
{uiSchema["ui:title"] || schema.title || label}
|
||||
{(label || uiSchema["ui:title"] || schema.title) && required ? "*" : null}
|
||||
</Form.Label>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
descWithChildren = <div className="row">
|
||||
{desc}
|
||||
<div className="col-12">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
desc = <OverlayTrigger
|
||||
trigger={["hover", "focus"]}
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={
|
||||
<Popover id={`popover-positioned-top`}>
|
||||
<Popover.Body>
|
||||
<Form.Text className={rawErrors.length > 0 ? "text-danger" : "text-muted"}
|
||||
dangerouslySetInnerHTML={{__html: rawDescription}}/>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}>
|
||||
<div className="col-4"
|
||||
style={{marginTop: (uiSchema.hasOwnProperty("ui:descLabelMT") && uiSchema["ui:descLabelMT"] ? '54px' : 'auto')}}>
|
||||
<Form.Label
|
||||
className={(showLabel ? 'col-12 ps-4' : 'd-none') + " col-form-label " + (rawErrors.length > 0 ? "text-danger" : "")}>
|
||||
{uiSchema["ui:title"] || schema.title || label}
|
||||
{(label || uiSchema["ui:title"] || schema.title) && required ? "*" : null}
|
||||
</Form.Label>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
descWithChildren = <div className="row">
|
||||
{desc}
|
||||
<div className="col-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
desc = <OverlayTrigger
|
||||
trigger={["hover", "focus"]}
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={
|
||||
<Popover id={`popover-positioned-top`}>
|
||||
<Popover.Body>
|
||||
<Form.Text className={rawErrors.length > 0 ? "text-danger" : "text-muted"}
|
||||
dangerouslySetInnerHTML={{__html: rawDescription}}/>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}>
|
||||
<div className="mx-2 py-2 descInfoBtn">
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
color={rawErrors.length > 0 ? "red" : ""}
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
descWithChildren = <div className="d-flex">
|
||||
{desc}
|
||||
<div className="w-100">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
descWithChildren = children
|
||||
}
|
||||
return (
|
||||
<WrapIfAdditional
|
||||
classNames={classNames}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
label={label}
|
||||
onDropPropertyClick={onDropPropertyClick}
|
||||
onKeyChange={onKeyChange}
|
||||
readonly={readonly}
|
||||
required={required}
|
||||
schema={schema}
|
||||
>
|
||||
<Form.Group style={uiSchema.hasOwnProperty('ui:style') ? uiSchema['ui:style'] : {}}>
|
||||
{descWithChildren}
|
||||
{rawErrors.length > 0 && (
|
||||
<ListGroup as="ul" className={descLabel ? (dlva ? 'ps-4' : ' col-8 offset-4') : ''}>
|
||||
{rawErrors.map((error: string) => {
|
||||
return (
|
||||
<ListGroup.Item as="li" key={error} className="border-0 m-0 p-0">
|
||||
<small className="m-0 text-danger">
|
||||
{error}
|
||||
</small>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
)}
|
||||
{rawHelp && (
|
||||
<Form.Text
|
||||
className={rawErrors.length > 0 ? "text-danger" : "text-muted"}
|
||||
id={id}>
|
||||
{rawHelp}
|
||||
</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
</WrapIfAdditional>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldTemplate;
|
@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
|
||||
import {utils} from "@rjsf/core";
|
||||
import {JSONSchema7} from "json-schema";
|
||||
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import IconButton from "../IconButton/IconButton";
|
||||
|
||||
const {ADDITIONAL_PROPERTY_FLAG} = utils;
|
||||
|
||||
type WrapIfAdditionalProps = {
|
||||
children: React.ReactElement;
|
||||
classNames: string;
|
||||
disabled: boolean;
|
||||
id: string;
|
||||
label: string;
|
||||
onDropPropertyClick: (index: string) => (event?: any) => void;
|
||||
onKeyChange: (index: string) => (event?: any) => void;
|
||||
readonly: boolean;
|
||||
required: boolean;
|
||||
schema: JSONSchema7;
|
||||
};
|
||||
|
||||
const WrapIfAdditional = ({
|
||||
children,
|
||||
disabled,
|
||||
id,
|
||||
label,
|
||||
onDropPropertyClick,
|
||||
onKeyChange,
|
||||
readonly,
|
||||
required,
|
||||
schema,
|
||||
}: WrapIfAdditionalProps) => {
|
||||
const keyLabel = `${label} Key`; // i18n ?
|
||||
const additional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
|
||||
|
||||
if (!additional) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const handleBlur = ({target}: React.FocusEvent<HTMLInputElement>) =>
|
||||
onKeyChange(target.value);
|
||||
|
||||
return (
|
||||
<Row key={`${id}-key`}>
|
||||
<Col xs={5}>
|
||||
<Form.Group>
|
||||
<Form.Label>{keyLabel}</Form.Label>
|
||||
<Form.Control
|
||||
required={required}
|
||||
defaultValue={label}
|
||||
disabled={disabled || readonly}
|
||||
id={`${id}-key`}
|
||||
name={`${id}-key`}
|
||||
onBlur={!readonly ? handleBlur : undefined}
|
||||
type="text"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
<Col xs={5}>
|
||||
{children}
|
||||
</Col>
|
||||
<Col xs={2} className="py-4">
|
||||
<IconButton
|
||||
// @ts-ignore
|
||||
block={"true"}
|
||||
className="w-100"
|
||||
variant="danger"
|
||||
icon="remove"
|
||||
tabIndex={-1}
|
||||
disabled={disabled || readonly}
|
||||
onClick={onDropPropertyClick(label)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrapIfAdditional;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./FieldTemplate";
|
||||
export * from "./FieldTemplate";
|
@ -0,0 +1,7 @@
|
||||
import DescriptionField from "../DescriptionField/DescriptionField";
|
||||
import TitleField from "../TitleField/TitleField";
|
||||
|
||||
export default {
|
||||
DescriptionField,
|
||||
TitleField,
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./Fields";
|
||||
export * from "./Fields";
|
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const FileWidget = (props: WidgetProps) => {
|
||||
const {registry} = props;
|
||||
const {TextWidget} = registry.widgets;
|
||||
return <TextWidget {...props} type="file"/>;
|
||||
};
|
||||
|
||||
export default FileWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./FileWidget";
|
||||
export * from "./FileWidget";
|
10
misc/config_tools/configurator/src/lib/bs4rjsf/Form/Form.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import {withTheme, FormProps} from "@rjsf/core";
|
||||
|
||||
import Theme from "../Theme";
|
||||
import {StatelessComponent} from "react";
|
||||
|
||||
const Form:
|
||||
| React.ComponentClass<FormProps<any>>
|
||||
| StatelessComponent<FormProps<any>> = withTheme(Theme);
|
||||
|
||||
export default Form;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./Form";
|
||||
export * from "./Form";
|
@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import Button, {ButtonProps} from "react-bootstrap/Button";
|
||||
import {IoIosRemove} from "react-icons/io";
|
||||
import {GrAdd} from "react-icons/gr";
|
||||
import {AiOutlineArrowUp, AiOutlineArrowDown} from "react-icons/ai";
|
||||
|
||||
const mappings: any = {
|
||||
remove: <IoIosRemove/>,
|
||||
plus: <GrAdd/>,
|
||||
"arrow-up": <AiOutlineArrowUp/>,
|
||||
"arrow-down": <AiOutlineArrowDown/>,
|
||||
};
|
||||
|
||||
type IconButtonProps = ButtonProps & {
|
||||
icon: string;
|
||||
variant?: ButtonProps["variant"];
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
style?: any;
|
||||
disabled?: any;
|
||||
onClick?: any;
|
||||
};
|
||||
|
||||
const IconButton = (props: IconButtonProps) => {
|
||||
const {icon, className, ...otherProps} = props;
|
||||
return (
|
||||
<Button {...otherProps} variant={props.variant || "light"} size="sm">
|
||||
{mappings[icon]}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconButton;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./IconButton";
|
||||
export * from "./IconButton";
|
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
|
||||
import Row from "react-bootstrap/Row";
|
||||
import Col from "react-bootstrap/Col";
|
||||
import Container from "react-bootstrap/Container";
|
||||
|
||||
import {ObjectFieldTemplateProps} from "@rjsf/core";
|
||||
import {utils} from "@rjsf/core";
|
||||
|
||||
import AddButton from "../AddButton/AddButton";
|
||||
|
||||
const {canExpand} = utils;
|
||||
|
||||
const ObjectFieldTemplate = (
|
||||
{
|
||||
DescriptionField,
|
||||
description,
|
||||
TitleField,
|
||||
title,
|
||||
properties,
|
||||
required,
|
||||
uiSchema,
|
||||
idSchema,
|
||||
schema,
|
||||
formData,
|
||||
onAddClick,
|
||||
disabled,
|
||||
readonly,
|
||||
}: ObjectFieldTemplateProps) => {
|
||||
let content = properties.map((element: any, index: number) => {
|
||||
return (
|
||||
<Col
|
||||
key={index}
|
||||
style={{marginBottom: "10px"}}
|
||||
className={element.hidden ? "d-none" : undefined}
|
||||
xs={(uiSchema[element.name] && uiSchema[element.name]['ui:grid']) ?
|
||||
uiSchema[element.name]['ui:grid'] :
|
||||
12
|
||||
}>
|
||||
{element.content}
|
||||
</Col>
|
||||
);
|
||||
})
|
||||
let expand = canExpand(schema, uiSchema, formData) ? (
|
||||
<Row>
|
||||
<Col xs={{offset: 9, span: 3}} className="py-4">
|
||||
<AddButton
|
||||
onClick={onAddClick(schema)}
|
||||
disabled={disabled || readonly}
|
||||
className="object-property-expand"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
|
||||
let container = <Container fluid className="p-0">
|
||||
<Row>{content}</Row>
|
||||
{expand}
|
||||
</Container>
|
||||
|
||||
return (
|
||||
<>
|
||||
{(uiSchema["ui:title"] || title) && (
|
||||
<TitleField
|
||||
id={`${idSchema.$id}-title`}
|
||||
title={uiSchema["ui:title"] || title}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
{description && (
|
||||
<DescriptionField
|
||||
id={`${idSchema.$id}-description`}
|
||||
description={description}
|
||||
/>
|
||||
)}
|
||||
{container}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectFieldTemplate;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./ObjectFieldTemplate";
|
||||
export * from "./ObjectFieldTemplate";
|
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const PasswordWidget = ({
|
||||
id,
|
||||
required,
|
||||
readonly,
|
||||
disabled,
|
||||
value,
|
||||
label,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChange,
|
||||
options,
|
||||
autofocus,
|
||||
schema,
|
||||
rawErrors = [],
|
||||
}: WidgetProps) => {
|
||||
const _onChange = ({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(value === "" ? options.emptyValue : value);
|
||||
const _onBlur = ({target: {value}}: React.FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, value);
|
||||
const _onFocus = ({
|
||||
target: {value},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
|
||||
|
||||
return (
|
||||
<Form.Group className="mb-0">
|
||||
<Form.Label className={rawErrors.length > 0 ? "text-danger" : ""}>
|
||||
{label || schema.title}
|
||||
{(label || schema.title) && required ? "*" : null}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
id={id}
|
||||
autoFocus={autofocus}
|
||||
className={rawErrors.length > 0 ? "is-invalid" : ""}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
type="password"
|
||||
value={value ? value : ""}
|
||||
onFocus={_onFocus}
|
||||
onBlur={_onBlur}
|
||||
onChange={_onChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./PasswordWidget";
|
||||
export * from "./PasswordWidget";
|
@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const RadioWidget = (
|
||||
{
|
||||
id,
|
||||
schema,
|
||||
options,
|
||||
value,
|
||||
required,
|
||||
disabled,
|
||||
readonly,
|
||||
label,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
uiSchema,
|
||||
}: WidgetProps) => {
|
||||
const {enumOptions, enumDisabled} = options;
|
||||
|
||||
const _onChange = ({target: {value},}: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(schema.type == "boolean" ? value !== "false" : value);
|
||||
const _onBlur = ({target: {value}}: React.FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, value);
|
||||
const _onFocus = ({target: {value},}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
|
||||
|
||||
const inline = Boolean(options && options.inline);
|
||||
|
||||
return (
|
||||
<Form.Group className="mb-0 row">
|
||||
<Form.Label className="col-sm-4 col-form-label">
|
||||
{uiSchema["ui:title"] || schema.title || label}
|
||||
{(label || uiSchema["ui:title"] || schema.title) && required ? "*" : null}
|
||||
</Form.Label>
|
||||
<div className="col-sm-8">
|
||||
{(enumOptions as any).map((option: any, i: number) => {
|
||||
const itemDisabled =
|
||||
Array.isArray(enumDisabled) &&
|
||||
enumDisabled.indexOf(option.value) !== -1;
|
||||
const checked = option.value == value;
|
||||
|
||||
// @ts-ignore
|
||||
const radio = (
|
||||
<Form.Check
|
||||
inline={inline}
|
||||
label={option.label}
|
||||
id={option.label}
|
||||
key={i}
|
||||
name={id}
|
||||
type="radio"
|
||||
disabled={disabled || itemDisabled || readonly}
|
||||
checked={checked}
|
||||
required={required}
|
||||
value={option.value}
|
||||
onChange={_onChange}
|
||||
onBlur={_onBlur}
|
||||
onFocus={_onFocus}
|
||||
/>
|
||||
);
|
||||
return radio;
|
||||
})}
|
||||
</div>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioWidget;
|
@ -0,0 +1,2 @@
|
||||
export {default} from "./RadioWidget";
|
||||
export * from "./RadioWidget";
|
@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import {utils} from "@rjsf/core";
|
||||
import {WidgetProps} from "@rjsf/core";
|
||||
|
||||
const {rangeSpec} = utils;
|
||||
|
||||
const RangeWidget = ({
|
||||
value,
|
||||
readonly,
|
||||
disabled,
|
||||
onBlur,
|
||||
onFocus,
|
||||
options,
|
||||
schema,
|
||||
onChange,
|
||||
required,
|
||||
label,
|
||||
id,
|
||||
uiSchema,
|
||||
}: WidgetProps) => {
|
||||
let sliderProps = {value, label, id, ...rangeSpec(schema)};
|
||||
|
||||
const _onChange = ({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(value === "" ? options.emptyValue : value);
|
||||
const _onBlur = ({target: {value}}: React.FocusEvent<HTMLInputElement>) =>
|
||||
onBlur(id, value);
|
||||
const _onFocus = ({
|
||||
target: {value},
|
||||
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
|
||||
|
||||
return (
|
||||
<Form.Group className="mb-0">
|
||||
<Form.Label>
|
||||
{uiSchema["ui:title"] || schema.title || label}
|
||||
{(label || uiSchema["ui:title"] || schema.title) && required ? "*" : null}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="range"
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
onChange={_onChange}
|
||||
onBlur={_onBlur}
|
||||
onFocus={_onFocus}
|
||||
{...sliderProps}
|
||||
/>
|
||||
<span className="range-view">{value}</span>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default RangeWidget;
|