init
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
8
.hintrc
Normal file
8
.hintrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"axe/forms": "off"
|
||||
}
|
||||
}
|
||||
131
DOCS.md
Normal file
131
DOCS.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Configucci — Документация проекта
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
**Configucci** — это веб-приложение на React для управления конфигурационными файлами в формате XML. Приложение позволяет загружать, редактировать и визуализировать конфигурации с поддержкой множественных окружений (environments) и шаблонизации параметров.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Технологический стек
|
||||
|
||||
| Категория | Технология |
|
||||
|-----------|------------|
|
||||
| **Фреймворк** | React 19.2.0 |
|
||||
| **Язык** | TypeScript 5.9 |
|
||||
| **Сборка** | Vite 7.2.4 |
|
||||
| **UI-библиотека** | Bootstrap 5.3.3 |
|
||||
| **Тестирование** | Vitest 4.0.16 + jsdom |
|
||||
| **Линтинг** | ESLint 9.17.0 |
|
||||
| **Подсветка кода** | react-highlight + highlight.js |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
configucci/
|
||||
├── src/
|
||||
│ ├── models/ # Модели данных
|
||||
│ │ ├── Config.tsx # Config, ConfigTemplate
|
||||
│ │ ├── ConfigReader.tsx # Парсер XML
|
||||
│ │ ├── Env.ts # Env, AppEvent, AddEvent, DelEvent, UpdateEvent
|
||||
│ │ ├── EnvParam.ts # EnvParam
|
||||
│ │ └── NamedId.ts # Интерфейс NamedId
|
||||
│ ├── componets/ # React-компоненты (орфография оригинала)
|
||||
│ │ ├── env/
|
||||
│ │ │ ├── Environment.tsx # Список окружений
|
||||
│ │ │ └── EnvironmentParam.tsx # Параметр окружения
|
||||
│ │ ├── content/
|
||||
│ │ │ └── Content.tsx # Вкладки с контентом
|
||||
│ │ └── FileChooser.tsx # Загрузка файлов
|
||||
│ ├── builders/ # Построители XML
|
||||
│ │ ├── EnvBuilder.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── assets/ # Статические ресурсы
|
||||
│ │ └── cgg.png
|
||||
│ ├── test/ # Тесты
|
||||
│ │ └── ConfigReader.test.ts
|
||||
│ ├── App.tsx # Главный компонент
|
||||
│ ├── App.css # Стили приложения
|
||||
│ ├── main.tsx # Точка входа
|
||||
│ └── index.css # Глобальные стили
|
||||
├── docs/
|
||||
│ └── config.json.xml # Пример конфигурации
|
||||
├── public/ # Публичные ресурсы
|
||||
├── index.html
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── vite.config.ts
|
||||
└── vitest.config.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Модели данных
|
||||
|
||||
### `Config`
|
||||
Основной класс конфигурации:
|
||||
- `envs: Env[]` — массив окружений
|
||||
- `template: ConfigTemplate` — шаблон конфигурации
|
||||
- Методы: `addEnvs()`, `addTemplate()`, `getTemplateAsJson()`, `validateParams()`
|
||||
|
||||
### `ConfigTemplate`
|
||||
Шаблон с параметрами в формате `@paramName@`:
|
||||
- Извлекает параметры из текста через regex `/@(\w+)@/g`
|
||||
- Свойства: `content` (исходный текст), `Params` (массив имён параметров)
|
||||
|
||||
### `Env`
|
||||
Окружение с параметрами:
|
||||
- `id: number`, `name: string`, `params: EnvParam[]`
|
||||
- Методы: `addParams()`, `delParam()`, `updateParams()`, `isDefault()`
|
||||
|
||||
### `EnvParam`
|
||||
Параметр окружения:
|
||||
- `id: number`, `name: string`, `value: string`, `isChanged: boolean`
|
||||
- Методы: `Changed()`, `sanitize()` (HTML-экранирование)
|
||||
|
||||
### `AppEvent<T>`
|
||||
Базовый класс событий для операций CRUD:
|
||||
- `AddEvent<T>` — добавление
|
||||
- `UpdateEvent<T>` — обновление
|
||||
- `DelEvent<T>` — удаление
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Основные компоненты
|
||||
|
||||
### `App` (App.tsx)
|
||||
Корневой компонент с управлением состоянием через `AppState`:
|
||||
- Загрузка конфигурации из файла
|
||||
- Редактирование окружений
|
||||
- Сохранение с искусственной задержкой 1 секунда
|
||||
|
||||
### `FileChooser`
|
||||
Компонент загрузки файлов:
|
||||
- Поддерживает только XML-файлы (`text/xml`)
|
||||
- Кнопка "Create new" создаёт конфигурацию с окружением DEFAULT
|
||||
|
||||
### `Environment`
|
||||
Список окружений с возможностью:
|
||||
- Выбор окружения из dropdown
|
||||
- Добавление/удаление/редактирование параметров
|
||||
- Визуальное отображение изменений (жёлтая рамка)
|
||||
|
||||
### `Content`
|
||||
Вкладки для просмотра данных:
|
||||
|
||||
| Вкладка | Описание |
|
||||
|---------|----------|
|
||||
| **Env** | XML-представление текущего окружения |
|
||||
| **Content Template** | JSON-шаблон с подсветкой синтаксиса |
|
||||
| **Raw template** | Полный XML с окружениями и шаблоном |
|
||||
| **Test-filled template** | Тестовый JavaScript-код (заглушка) |
|
||||
|
||||
---
|
||||
|
||||
## 📄 Формат конфигурационного файла
|
||||
|
||||
```xml
|
||||
<engine>
|
||||
<environment name="DEFAULT">
|
||||
|
||||
174
QWEN.md
Normal file
174
QWEN.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Configucci — Project Context
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Configucci** is a React-based web application for managing XML configuration files. It enables users to load, edit, and visualize configurations with support for multiple environments and parameter templating.
|
||||
|
||||
### Core Features
|
||||
- Load XML configuration files with multiple environments
|
||||
- Edit environment parameters with change tracking (yellow border indicates modifications)
|
||||
- Template system using @paramName@ syntax for dynamic substitution
|
||||
- Visual preview of configurations in multiple formats (XML, JSON, Raw)
|
||||
- Bootstrap 5-based responsive UI
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Category | Technology |
|
||||
|----------|------------|
|
||||
| **Framework** | React 19.2.0 |
|
||||
| **Language** | TypeScript 5.9 |
|
||||
| **Build Tool** | Vite 7.2.4 |
|
||||
| **UI Library** | Bootstrap 5.3.3 |
|
||||
| **Testing** | Vitest 4.0.16 + jsdom |
|
||||
| **Linting** | ESLint 9.17.0 + typescript-eslint |
|
||||
| **Syntax Highlighting** | react-highlight + highlight.js |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
configucci/
|
||||
├── src/
|
||||
│ ├── models/ # Data models and domain logic
|
||||
│ │ ├── Config.tsx # Config, ConfigTemplate classes
|
||||
│ │ ├── ConfigReader.tsx # XML parser utility
|
||||
│ │ ├── Env.ts # Env, AppEvent, AddEvent, DelEvent, UpdateEvent
|
||||
│ │ ├── EnvParam.ts # EnvParam class
|
||||
│ │ └── NamedId.ts # NamedId interface
|
||||
│ ├── componets/ # React components (note: directory name typo)
|
||||
│ │ ├── env/
|
||||
│ │ │ ├── Environment.tsx # Environment list & editor
|
||||
│ │ │ └── EnvironmentParam.tsx # Single parameter editor
|
||||
│ │ ├── content/
|
||||
│ │ │ └── Content.tsx # Tabbed content viewer
|
||||
│ │ └── FileChooser.tsx # File upload component
|
||||
│ ├── builders/ # XML builders
|
||||
│ │ ├── EnvBuilder.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── assets/ # Static assets (images, etc.)
|
||||
│ ├── test/ # Unit tests
|
||||
│ │ └── ConfigReader.test.ts
|
||||
│ ├── App.tsx # Root component
|
||||
│ ├── App.css # Application styles
|
||||
│ ├── main.tsx # Entry point
|
||||
│ └── index.css # Global styles
|
||||
├── docs/
|
||||
│ └── config.json.xml # Example configuration file
|
||||
├── public/ # Public static assets
|
||||
├── index.html
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── vite.config.ts
|
||||
└── vitest.config.ts
|
||||
```
|
||||
|
||||
## Key Data Models
|
||||
|
||||
### Config
|
||||
Main configuration container:
|
||||
- `envs: Env[]` — Array of environments
|
||||
- `template: ConfigTemplate` — Template with parameter placeholders
|
||||
- Methods: `addEnvs()`, `addTemplate()`, `getTemplateAsJson()`, `validateParams()`
|
||||
|
||||
### ConfigTemplate
|
||||
Template with @paramName@ placeholders:
|
||||
- Extracts parameters via regex `/@(\w+)@/g`
|
||||
- Properties: `content` (raw text), `Params` (array of parameter names)
|
||||
|
||||
### Env
|
||||
Environment definition:
|
||||
- `id: number`, `name: string`, `params: EnvParam[]`
|
||||
- Methods: `addParams()`, `delParam()`, `updateParams()`, `isDefault()`
|
||||
|
||||
### EnvParam
|
||||
Environment parameter:
|
||||
- `id: number`, `name: string`, `value: string`, `isChanged: boolean`
|
||||
- Methods: `Changed()`, `sanitize()` (HTML escaping)
|
||||
|
||||
### AppEvent<T>
|
||||
Base class for CRUD operations:
|
||||
- `AddEvent<T>`, `UpdateEvent<T>`, `DelEvent<T>` subclasses
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Starts Vite dev server with HMR on 0.0.0.0:5173 (accessible externally).
|
||||
|
||||
### Build
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
Compiles TypeScript and bundles with Vite. Output in `dist/`.
|
||||
|
||||
### Preview Production Build
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Linting
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
Runs Vitest tests with jsdom environment.
|
||||
|
||||
## Configuration File Format
|
||||
|
||||
XML configuration files follow this structure:
|
||||
|
||||
- Root element: `engine`
|
||||
- Environment elements: `environment name="..."` containing `parameter` children
|
||||
- Template element: `template` containing JSON/text with @param@ placeholders
|
||||
|
||||
See `docs/config.json.xml` for a complete example.
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### App (Root)
|
||||
Manages global state via `AppState` singleton. Handles:
|
||||
- File loading through `FileChooser`
|
||||
- Environment editing through `Environment`
|
||||
- Content preview through `Content`
|
||||
|
||||
### FileChooser
|
||||
File upload component supporting only XML files (`text/xml`). "Create new" button initializes a default configuration.
|
||||
|
||||
### Environment
|
||||
Displays environment list (dropdown) and parameter editor. Features:
|
||||
- Add/delete/update parameters
|
||||
- Visual change tracking (yellow border for modified values)
|
||||
- DEFAULT environment is special (cannot be deleted)
|
||||
|
||||
### Content
|
||||
Tabbed viewer with four tabs:
|
||||
1. **Env** - XML representation of current environment
|
||||
2. **Content Template** - JSON template with syntax highlighting
|
||||
3. **Raw template** - Full XML with all environments and template
|
||||
4. **Test-filled template** - JavaScript test code (stub)
|
||||
|
||||
## State Management
|
||||
|
||||
Uses `AppState` singleton pattern:
|
||||
- Stores current `Config` and `Env[]`
|
||||
- `saveEnv()` has artificial 1-second delay (simulates async persistence)
|
||||
- Components subscribe via React `useState` hooks
|
||||
|
||||
## Development Conventions
|
||||
|
||||
- **TypeScript**: Strict typing with solution-style references (`tsconfig.app.json`, `tsconfig.node.json`)
|
||||
- **ESLint**: Uses `typescript-eslint` with React hooks and refresh plugins
|
||||
- **Testing**: Vitest with jsdom, tests in `src/test/`
|
||||
- **Naming**: Component files use PascalCase, models use PascalCase
|
||||
- **Note**: Directory is named `componets` (typo preserved for consistency)
|
||||
|
||||
## Known Quirks
|
||||
|
||||
1. Directory `src/componets/` has a typo (should be `components/`)
|
||||
2. `ConfigReader.tsx` has `.tsx` extension but contains no JSX
|
||||
3. Some model files use `.ts` while others use `.tsx`
|
||||
50
README.md
Normal file
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
BIN
cgg-ico copy.png
Normal file
BIN
cgg-ico copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
3
docs/config.json.xml
Normal file
3
docs/config.json.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<engine>
|
||||
<environment name="DEFAULT">
|
||||
|
||||
10
docs/file_format_spec.md
Normal file
10
docs/file_format_spec.md
Normal file
@@ -0,0 +1,10 @@
|
||||
The `config.json.xml` file defines environment-specific configurations and a template for generating JSON output. Key syntax points:
|
||||
|
||||
- `<environment name="...">`: Defines a named environment (e.g., `DEFAULT`, `env1`, `env2`).
|
||||
- `<parameter name="..." value="..."/>`: Sets key-value pairs for parameters (e.g., `host`, `port`, `MessageBrokerHosts`).
|
||||
- `@param_name@`: Placeholder in the template that gets replaced with actual parameter values.
|
||||
- `@env_name@`: Placeholder replaced with the current environment name.
|
||||
- `"[ ... ]"`: Array syntax for list values like `MessageBrokerHosts`.
|
||||
- Template uses JSON format with placeholders for dynamic substitution.
|
||||
|
||||
The template generates a final JSON config by replacing all `@...@` placeholders with corresponding parameter values from the active environment.
|
||||
184
docs/main.md
Normal file
184
docs/main.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Project Guide: XML Environment Configuration Editor
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
This is a **React-based web application** designed to manage environment configurations stored in XML format. It allows users to:
|
||||
- Load `.xml` configuration files (via file picker)
|
||||
- View and edit environment variables
|
||||
- Save changes with simulated persistence delay
|
||||
- Generate valid XML output from in-memory data
|
||||
|
||||
The core idea is to provide a **visual editor for XML-based engine configurations**, particularly useful for applications that rely on structured environment definitions.
|
||||
|
||||
[File format](file_format_spec.md)
|
||||
|
||||
### Key Technologies
|
||||
- **TypeScript**: Full type safety across the codebase
|
||||
- **React**: Component-based UI architecture
|
||||
- **Vite**: Fast development server and build system
|
||||
- **XML Parsing & Serialization**: Custom logic in `ConfigReader.tsx` and `EnvBuilder.ts`
|
||||
|
||||
### High-Level Architecture
|
||||
```
|
||||
User Interface (React)
|
||||
│
|
||||
▼
|
||||
File Chooser → ConfigReader → Config (JSON-like) → AppState
|
||||
│ │ │
|
||||
└─────────→ EnvBuilder ←─┘ │
|
||||
(Save/Update)│
|
||||
▼
|
||||
State Update (async)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (v16 or higher)
|
||||
- npm or pnpm
|
||||
- A modern browser (Chrome/Firefox)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Clone the repo (if not already done)
|
||||
git clone <your-repo-url>
|
||||
cd your-project
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
No test files (`*.test.ts`) are present in the current codebase. Consider adding Jest or Vitest for future testing.
|
||||
|
||||
### Basic Usage
|
||||
1. Open the app in your browser.
|
||||
2. Click "Create new" to initialize a default config with an `DEFAULT` environment.
|
||||
3. Upload an `.xml` file using the file picker.
|
||||
4. Edit environments and parameters in the UI.
|
||||
5. Changes are saved asynchronously (simulated 1-second delay).
|
||||
|
||||
---
|
||||
|
||||
## 3. Project Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|------------------|--------|
|
||||
| `src/` | Main source code |
|
||||
| `src/App.tsx` | Central state manager (`AppState`) and config loading/saving logic |
|
||||
| `src/main.tsx` | React root rendering entry point |
|
||||
| `src/models/` | Core data models and parsers (`Env`, `EnvParam`, `ConfigReader`) |
|
||||
| `src/builders/` | Utilities to convert objects into XML strings (`EnvBuilder`) |
|
||||
| `src/components/`| UI components (`FileChoiser`, `Content`) |
|
||||
| `src/vite-env.d.ts` | Vite environment type definitions |
|
||||
|
||||
### Key Files
|
||||
- `App.tsx`: Manages global state (`AppState.Instance`) and handles config loading/saving.
|
||||
- `ConfigReader.tsx`: Parses XML into `Config` object. Uses DOM API to traverse nodes.
|
||||
- `EnvBuilder.ts`: Serializes `Env` objects into valid XML strings using template literals.
|
||||
- `FileChoiser.tsx`: Handles file input and creation of new configs.
|
||||
|
||||
---
|
||||
|
||||
## 4. Development Workflow
|
||||
|
||||
### Coding Standards
|
||||
- Use TypeScript with strict type checking (enforced by `tsconfig.json`)
|
||||
- Follow React functional component patterns
|
||||
- Use `async/await` for I/O operations (e.g., file parsing, saving)
|
||||
- Avoid side effects in pure functions
|
||||
|
||||
### Testing
|
||||
Currently no tests are included. Add:
|
||||
```bash
|
||||
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
|
||||
```
|
||||
Then create test files in `__tests__/`.
|
||||
|
||||
### Build & Deployment
|
||||
- Build: `npm run build`
|
||||
- Output: `dist/` directory
|
||||
- Deploy static files via GitHub Pages, Vercel, Netlify, etc.
|
||||
|
||||
### Contribution Guidelines
|
||||
- Fork the repo and create a feature branch
|
||||
- Write descriptive commit messages
|
||||
- Open a PR with clear summary of changes
|
||||
- Ensure no breaking changes without version bump
|
||||
|
||||
---
|
||||
|
||||
## 5. Key Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|--------|-------------|
|
||||
| `Env` | Represents an environment (e.g., "DEV", "PROD") with a name and list of parameters |
|
||||
| `EnvParam` | A key-value pair representing a configuration variable |
|
||||
| `Config` | Root container for multiple `Env` objects |
|
||||
| `ConfigReader` | Parses XML files into `Config` objects using DOM traversal |
|
||||
| `EnvBuilder` | Converts `Env` objects back into valid XML strings |
|
||||
| `AppState` | Singleton managing global state (currently only holds `envs`) |
|
||||
|
||||
### Design Patterns
|
||||
- **Singleton**: `AppState.Instance`
|
||||
- **Builder Pattern**: `EnvBuilder` for generating structured XML output
|
||||
- **Observer/State Management**: `saveEnv()` updates state and logs, simulates async persistence
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Tasks
|
||||
|
||||
### Load a Config File
|
||||
1. Click the file input button in the top-right.
|
||||
2. Select an `.xml` file that follows the expected structure:
|
||||
```xml
|
||||
<engine>
|
||||
<environment name="DEV">
|
||||
<parameter name="API_URL" value="http://localhost:3000" />
|
||||
</environment>
|
||||
</engine>
|
||||
```
|
||||
3. The app parses and loads it into memory.
|
||||
|
||||
### Create a New Config
|
||||
1. Click **"Create new"** in the `FileChoiser`.
|
||||
2. A default config with one environment (`DEFAULT`) is created.
|
||||
3. You can now edit or save it.
|
||||
|
||||
### Save an Environment
|
||||
- Call `AppState.Instance.saveEnv(env)` when updating.
|
||||
- Returns a promise that resolves after 1 second (simulates backend delay).
|
||||
- Index of the updated env is returned (`-1` if not found).
|
||||
|
||||
### Export to XML
|
||||
- Use `new EnvBuilder().src(env).build()` to generate XML string from an `Env`.
|
||||
- Useful for saving or sharing configurations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|------|----------|
|
||||
| File not loading | Ensure the file is `.xml` and valid (check structure in `ConfigReader`) |
|
||||
| Missing `envs` after load | Verify `ConfigReader.parse()` returns a non-null `Config` |
|
||||
| "Invalid Chalk template style argument" error | This comes from `Content.tsx` — likely a typo or placeholder. Should be replaced with proper error message. |
|
||||
| XML not rendering correctly | Check that `EnvBuilder` uses correct escaping (`\r\n`, proper tags) |
|
||||
|
||||
> 💡 Tip: Use browser DevTools to inspect the DOM and console logs during file parsing.
|
||||
|
||||
---
|
||||
|
||||
## 8. References
|
||||
|
||||
- [Vite Documentation](https://vitejs.dev/guide/)
|
||||
- [React Docs](https://react.dev/)
|
||||
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
|
||||
- [XML DOM API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/XML_DOM)
|
||||
|
||||
> 🔗 **Note**: The XML schema used is not documented. You may want to define a `.xsd` or schema file for validation.
|
||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/cgg-ico.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{configucci}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4151
package-lock.json
generated
Normal file
4151
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "vite-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-highlight": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^16.5.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
BIN
public/cgg-ico.png
Normal file
BIN
public/cgg-ico.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
0
src/App.css
Normal file
0
src/App.css
Normal file
93
src/App.tsx
Normal file
93
src/App.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import { Env } from './models/Env'
|
||||
import Environment from "./componets/env"
|
||||
import Content from './componets/content'
|
||||
import { FileChooser } from './componets/FileChooser'
|
||||
import { Config } from "./models/Config"
|
||||
import logo from './assets/cgg.png'
|
||||
|
||||
class AppState {
|
||||
private constructor(
|
||||
public config: Config = new Config(),
|
||||
public envs: Env[] = [
|
||||
],
|
||||
) { }
|
||||
|
||||
static readonly Instance = new AppState();
|
||||
|
||||
public loadConfig(cfg: Config) {
|
||||
this.envs = [...cfg.envs];
|
||||
this.config = cfg;
|
||||
}
|
||||
|
||||
public async saveEnv(env: Env): Promise<number> {
|
||||
|
||||
// Create a promise that resolves after 1 second
|
||||
return await new Promise<number>((resolve) => {
|
||||
setTimeout(() => {
|
||||
let idx = this.envs.findIndex(x => x.id === env.id);
|
||||
if (idx > -1) {
|
||||
this.envs[idx] = env;
|
||||
console.log("UPDATED envs", this.envs);
|
||||
}
|
||||
resolve(idx); // Resolve the promise after updating
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [envs, setEnvs] = useState(AppState.Instance.envs);
|
||||
const [selectedEnv, setSelectedEnv] = useState(0);
|
||||
const [config, setConfig] = useState(AppState.Instance.config);
|
||||
|
||||
async function handleEnvChanged(env: Env) {
|
||||
let idx = await AppState.Instance.saveEnv(env);
|
||||
if (idx > -1) {
|
||||
setEnvs([...AppState.Instance.envs]);
|
||||
setSelectedEnv(idx);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="container-fluid m-2">
|
||||
<div className="row mb-2">
|
||||
<FileChooser onSelected={x => {
|
||||
AppState.Instance.loadConfig(x);
|
||||
setEnvs(AppState.Instance.envs);
|
||||
setConfig(AppState.Instance.config);
|
||||
}} />
|
||||
</div>
|
||||
{envs.length > 0 ?
|
||||
(<div className="row">
|
||||
<section id="env" className='col-4 me-1'>
|
||||
<Environment
|
||||
envs={envs}
|
||||
onChanged={async (e) => await handleEnvChanged(e)}
|
||||
onSelected={x => setSelectedEnv(x)} />
|
||||
</section>
|
||||
<section id="content" className="col-8 col-xl-7 border-start ms-1">
|
||||
<Content env={envs[selectedEnv]} config={config} />
|
||||
</section>
|
||||
</div>)
|
||||
:
|
||||
(
|
||||
<div className="row justify-content-center pt-5" >
|
||||
<div className="col-1 pt-5">
|
||||
<img src={logo} alt="" style={{ opacity: 0.2, transform: 'scale(1.8)' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
|
||||
BIN
src/assets/cgg.png
Normal file
BIN
src/assets/cgg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
62
src/builders/EnvBuilder.ts
Normal file
62
src/builders/EnvBuilder.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IBuilder } from ".";
|
||||
import { Env } from "../models/Env";
|
||||
|
||||
export class EnvBuilder implements IBuilder<Env> {
|
||||
|
||||
readonly ident = " ";
|
||||
readonly newLine = "\r\n";
|
||||
private stack: string[] = [];
|
||||
private _src!: Env;
|
||||
|
||||
|
||||
get src(): Env {
|
||||
return this._src;
|
||||
}
|
||||
set src(v: Env) {
|
||||
this._src = v;
|
||||
}
|
||||
|
||||
build(): string {
|
||||
return this
|
||||
.open()
|
||||
.params()
|
||||
.close()
|
||||
.toString();
|
||||
}
|
||||
|
||||
private params(): this {
|
||||
const tag = `<parameter name="{name}" value="{val}" />`;
|
||||
for (let p of this.src.params) {
|
||||
this.stack.push(this.ident);
|
||||
this.stack.push(tag
|
||||
.replace("{name}", p.name ?? "!ERR!")
|
||||
.replace("{val}", p.sanitize(p.value) ?? "!ERR!")
|
||||
);
|
||||
this.stack.push(this.newLine);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private open(): EnvBuilder {
|
||||
const tag = `<environment name="${this.src.name}">`;
|
||||
this.stack.push(tag);
|
||||
this.stack.push(this.newLine);
|
||||
return this;
|
||||
}
|
||||
private close(): EnvBuilder {
|
||||
const tag = `</environment>`;
|
||||
this.stack.push(tag);
|
||||
return this;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
let res = "";
|
||||
|
||||
while (this.stack.length > 0) {
|
||||
res = this.stack.pop() + res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
26
src/builders/index.ts
Normal file
26
src/builders/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Env } from "../models/Env";
|
||||
import { EnvBuilder } from "./EnvBuilder";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface IBuilder<T> {
|
||||
get src(): T;
|
||||
set src(v: T);
|
||||
|
||||
build(): string;
|
||||
}
|
||||
|
||||
export class Builder {
|
||||
public static getEnv(env: Env): IBuilder<Env> {
|
||||
let b = new EnvBuilder();
|
||||
b.src = env;
|
||||
return b;
|
||||
};
|
||||
|
||||
public static getEnvs(envs: Env[]): string {
|
||||
return envs.map(x => Builder.getEnv(x).build()).join("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
37
src/componets/FileChooser.tsx
Normal file
37
src/componets/FileChooser.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Env } from "../models/Env";
|
||||
import { ConfigReader } from "../models/ConfigReader";
|
||||
import { Config } from "../models/Config";
|
||||
|
||||
|
||||
export function FileChooser(props: { onSelected: (x: Config) => void }) {
|
||||
async function handleFile(x: React.ChangeEvent<HTMLInputElement>) {
|
||||
let file = x.target.files![0];
|
||||
|
||||
console.log(file.name, file.type, file.size, "supported:", ConfigReader.isSupportedFormat(file));
|
||||
let reader = new ConfigReader();
|
||||
let cfg = await reader.parseFromFile(file);
|
||||
|
||||
if (cfg !== null) {
|
||||
props.onSelected(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNew(){
|
||||
let cfg = new Config();
|
||||
cfg.addEnvs([new Env(0,"DEFAULT", [])]);
|
||||
props.onSelected(cfg);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-2">
|
||||
<button className="btn btn-primary" onClick={handleNew} >Create new</button>
|
||||
</div>
|
||||
<div className="col-1">or</div>
|
||||
|
||||
<div className="col">
|
||||
<input className="form-control" type="file" id="formFile" onChange={handleFile} />
|
||||
</div >
|
||||
</>
|
||||
);
|
||||
}
|
||||
4
src/componets/content/Content.css
Normal file
4
src/componets/content/Content.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.highlihgt-scrolled {
|
||||
overflow-x: auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
162
src/componets/content/Content.tsx
Normal file
162
src/componets/content/Content.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useState } from "react";
|
||||
import { Env } from "../../models/Env";
|
||||
import Highlight from 'react-highlight'
|
||||
import 'highlight.js/styles/far.css'
|
||||
import { Builder } from "../../builders";
|
||||
import { Config } from "../../models/Config";
|
||||
|
||||
|
||||
export function Content(props: { config: Config, env: Env }) {
|
||||
const [selectTab, setTab] = useState(ContentType.Env);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentTabs onSelected={(id) => setTab(id)} />
|
||||
<div className="">
|
||||
{selectTab == ContentType.Env ? (<ContentParams env={props.env} />) : ""}
|
||||
{selectTab == ContentType.Json ? (<ContentTemplate env={props.env} config={props.config} />) : ""}
|
||||
{selectTab == ContentType.Raw ? (<ContentRaw config={props.config} env={props.env} />) : ""}
|
||||
{selectTab == ContentType.Test ? (<ContentTest config={props.config} env={props.env} />) : ""}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
enum ContentType {
|
||||
Env = 0,
|
||||
Json = 1,
|
||||
Raw = 2,
|
||||
Test = 3
|
||||
}
|
||||
|
||||
function ContentTabs(props: { onSelected: (id: ContentType) => void }) {
|
||||
const [selectTab, setSelect] = useState(ContentType.Env);
|
||||
|
||||
function clickHandler(type: ContentType) {
|
||||
setSelect(type);
|
||||
props.onSelected(type);
|
||||
}
|
||||
|
||||
function isActive(type: ContentType): string {
|
||||
return type == selectTab ? " active" : " ";
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="nav nav-pills nav-fill">
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + isActive(ContentType.Env)} aria-current="page" href="#" onClick={() => clickHandler(ContentType.Env)}>Env</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + isActive(ContentType.Json)} href="#" onClick={() => clickHandler(ContentType.Json)} >Content Template</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + isActive(ContentType.Raw)} href="#" onClick={() => clickHandler(ContentType.Raw)}>Raw template</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className={"nav-link" + isActive(ContentType.Test)} href="#" onClick={() => clickHandler(ContentType.Test)}>Test-filled template</a>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentRaw(props: { config: Config, env: Env }) {
|
||||
const envsXml = Builder.getEnvs(props.config.envs);
|
||||
const templateContent = props.config.template.content;
|
||||
|
||||
const xml = `<engine>
|
||||
${envsXml}
|
||||
<template>
|
||||
${templateContent}
|
||||
</template>
|
||||
</engine>`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Highlight className="language-xml">
|
||||
{xml}
|
||||
</Highlight>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentTest(props: { config: Config, env: Env }) {
|
||||
const [selectedEnvId, setSelectedEnvId] = useState(props.env.id);
|
||||
const selectedEnv = props.config.envs.find(e => e.id === selectedEnvId) ?? props.env;
|
||||
|
||||
const filledTemplate = fillTemplate(props.config, selectedEnv);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Select Environment:</label>
|
||||
<select
|
||||
className="form-select w-auto d-inline-block"
|
||||
value={selectedEnvId}
|
||||
onChange={(e) => setSelectedEnvId(Number(e.target.value))}
|
||||
>
|
||||
{props.config.envs.map(env => (
|
||||
<option key={env.id} value={env.id}>{env.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Highlight className="language-json">
|
||||
{filledTemplate}
|
||||
</Highlight>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function fillTemplate(config: Config, env: Env): string {
|
||||
const defaultEnv = config.envs.find(e => e.name === "DEFAULT");
|
||||
const paramMap = new Map<string, string>();
|
||||
|
||||
// First, load DEFAULT values as fallback
|
||||
if (defaultEnv) {
|
||||
for (const param of defaultEnv.params) {
|
||||
if (param.name && param.value !== undefined) {
|
||||
paramMap.set(param.name, param.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, override with selected environment values (precedence)
|
||||
for (const param of env.params) {
|
||||
if (param.name && param.value !== undefined) {
|
||||
paramMap.set(param.name, param.value);
|
||||
}
|
||||
}
|
||||
|
||||
let filledTemplate = config.template.content;
|
||||
const placeholderRegex = /@(\w+)@/g;
|
||||
|
||||
filledTemplate = filledTemplate.replace(placeholderRegex, (match, paramName) => {
|
||||
if (paramName === Config.ENV_NAME_PARAM) {
|
||||
return env.name ?? "--NO-VALUE--";
|
||||
}
|
||||
return paramMap.get(paramName) ?? "--NO-VALUE--";
|
||||
});
|
||||
|
||||
return filledTemplate;
|
||||
}
|
||||
|
||||
function ContentTemplate(props: { config: Config, env: Env }) {
|
||||
|
||||
let text = props.config.getTemplateAsJson() ?? "{//no content. at all. tottaly empty!!!\n}";
|
||||
return (
|
||||
<>
|
||||
<Highlight className="language-json" >
|
||||
{text ?? ""}
|
||||
</Highlight>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentParams(props: { env: Env }) {
|
||||
const bldr = Builder.getEnv(props.env);
|
||||
|
||||
return (
|
||||
<Highlight className="language-xml">
|
||||
{bldr.build()}
|
||||
</Highlight>
|
||||
)
|
||||
}
|
||||
3
src/componets/content/index.tsx
Normal file
3
src/componets/content/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Content } from "./Content";
|
||||
|
||||
export default Content;
|
||||
72
src/componets/env/Environment.tsx
vendored
Normal file
72
src/componets/env/Environment.tsx
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState } from "react";
|
||||
import { AddEvent, AppEvent, DelEvent, Env, UpdateEvent } from "../../models/Env";
|
||||
import { EnvParam } from "../../models/EnvParam";
|
||||
import { EnvironmentParam } from "./EnvironmentParam";
|
||||
|
||||
export function Environment(props: { envs: Env[], onChanged: (env: Env) => void, onSelected: (envId: number) => void }) {
|
||||
const [currEnv, setCurrEnv] = useState(props.envs[0]);
|
||||
|
||||
function handleParamChanged(e: AppEvent<EnvParam>) {
|
||||
let isChanged = false;
|
||||
let env = currEnv;
|
||||
|
||||
if (e instanceof DelEvent) {
|
||||
env = currEnv.delParam(e.payload);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (e instanceof AddEvent) {
|
||||
env = currEnv.addParams(e.payload);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (e instanceof UpdateEvent) {
|
||||
env = currEnv.updateParams(e.payload);
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
if (isChanged) {
|
||||
let idx = props.envs.findIndex(x => x.id === env.id);
|
||||
if (idx > -1) {
|
||||
props.envs[idx] = env;
|
||||
props.onChanged(props.envs[idx]);
|
||||
setCurrEnv(env);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectOptions = props.envs.map((x) => <option key={x.id} value={x.id} >{x.name}</option>);
|
||||
const paramCtrls = currEnv.params.map(x =>
|
||||
<EnvironmentParam key={`${currEnv.id}-${x.id}`}
|
||||
param={new EnvParam(x.id, x.name, x.value)}
|
||||
onChanged={handleParamChanged}
|
||||
isNew={false} />);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<select
|
||||
id="environments"
|
||||
name="environments"
|
||||
aria-label="Environments"
|
||||
className="form-select"
|
||||
onChange={x => {
|
||||
let id = Number.parseInt(x.target.value);
|
||||
setCurrEnv(props.envs[id]);
|
||||
props.onSelected(id);
|
||||
}}>
|
||||
{selectOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="row">Params</div>
|
||||
{paramCtrls}
|
||||
<EnvironmentParam key={`${currEnv.id}-new`}
|
||||
param={new EnvParam(-1, "", "")}
|
||||
onChanged={handleParamChanged}
|
||||
isNew={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
68
src/componets/env/EnvironmentParam.tsx
vendored
Normal file
68
src/componets/env/EnvironmentParam.tsx
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState } from "react";
|
||||
import { EnvParam } from "../../models/EnvParam";
|
||||
import { AppEvent } from "../../models/Env";
|
||||
|
||||
|
||||
export function EnvironmentParam(props: { param: EnvParam; onChanged: (e: AppEvent<EnvParam>) => void, isNew: boolean }) {
|
||||
const [param, setParam] = useState(props.param);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
function doSet(x: string, act: (x: string) => void) {
|
||||
act(x);
|
||||
setParam(param.Changed(true));
|
||||
}
|
||||
|
||||
function handleChange() {
|
||||
if (!param.isChanged)
|
||||
return;
|
||||
|
||||
let newParam = param.Changed(false);
|
||||
if (!props.isNew) {
|
||||
props.onChanged(AppEvent.update(newParam));
|
||||
}
|
||||
|
||||
setParam(newParam);
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
props.onChanged(AppEvent.add(param));
|
||||
setParam(new EnvParam(0, "", ""));
|
||||
}
|
||||
|
||||
function handleKeyUp(x: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (x.key === "Enter") { handleChange(); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"row px-0" + (param.isChanged ? "border border-warning" : "")}
|
||||
style={isFocused ? { backgroundColor: "lightskyblue", padding: "1px 0" } : { padding: "1px 0" }}>
|
||||
<div className="col-4 mx-0 px-0">
|
||||
<input type="text"
|
||||
className="form-control"
|
||||
style={{ backgroundColor: "rgba(170, 170, 247, 0.16)" }}
|
||||
value={param.name}
|
||||
onChange={x => doSet(x.target.value, (v) => param.name = v)}
|
||||
onBlur={() => { handleChange(); setIsFocused(false); }}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyUp={handleKeyUp}
|
||||
placeholder="name"
|
||||
aria-label="name" />
|
||||
</div>
|
||||
<div className="col mx-0 px-0">
|
||||
<input type="text"
|
||||
className="form-control"
|
||||
value={param.value}
|
||||
onChange={x => doSet(x.target.value, v => param.value = v)}
|
||||
onBlur={() => { handleChange(); setIsFocused(false); }}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyUp={handleKeyUp}
|
||||
placeholder="value"
|
||||
aria-label="value" />
|
||||
</div>
|
||||
<div className="col-1 mx-0 px-0" >
|
||||
<button className="btn btn-success" hidden={!props.isNew} onClick={handleAdd}>✓</button>
|
||||
<button className="btn btn-warning" hidden={props.isNew} onClick={() => props.onChanged(AppEvent.del(param))} tabIndex={-1}>−</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/componets/env/index.tsx
vendored
Normal file
3
src/componets/env/index.tsx
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Environment } from "./Environment";
|
||||
|
||||
export default Environment;
|
||||
0
src/index.css
Normal file
0
src/index.css
Normal file
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
74
src/models/Config.tsx
Normal file
74
src/models/Config.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Env } from "./Env";
|
||||
|
||||
|
||||
export class ConfigTemplate {
|
||||
public static Empty: ConfigTemplate = new ConfigTemplate();
|
||||
|
||||
constructor(text: string = "") {
|
||||
this._contentText = text;
|
||||
this.extractParams();
|
||||
}
|
||||
|
||||
private _contentText: string = "";
|
||||
private _params: string[] = [];
|
||||
|
||||
public get content(): string {
|
||||
return this._contentText;
|
||||
}
|
||||
|
||||
public get Params(): string[] {
|
||||
return [...this._params];
|
||||
}
|
||||
|
||||
private extractParams() {
|
||||
let regex = /@(\w+)@/g;
|
||||
let matches;
|
||||
let paramsSet = new Set<string>();
|
||||
|
||||
while ((matches = regex.exec(this._contentText)) !== null) {
|
||||
if (matches.length > 1) {
|
||||
paramsSet.add(matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
this._params = Array.from(paramsSet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class Config {
|
||||
public static get ENV_NAME_PARAM(): string { return "env_name" };
|
||||
|
||||
public envs: Env[] = [];
|
||||
public template: ConfigTemplate = ConfigTemplate.Empty;
|
||||
|
||||
addEnvs(envs: Env[]) {
|
||||
this.envs = envs;
|
||||
}
|
||||
|
||||
addTemplate(text: string) {
|
||||
this.template = new ConfigTemplate(text);
|
||||
}
|
||||
|
||||
getTemplateAsJson(): string {
|
||||
try {
|
||||
return this.template.content ;
|
||||
} catch (error) {
|
||||
console.error("Error converting template content to JSON:", error);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
validateParams(): string[] {
|
||||
const envKeys = this.envs.map(env => env.params.map(param => param.name)).flat();
|
||||
const missingParams = this.template.Params.filter(param => param != Config.ENV_NAME_PARAM && !envKeys.includes(param));
|
||||
|
||||
if (missingParams.length > 0) {
|
||||
console.error("Template: missing parameters in environments:", missingParams);
|
||||
}
|
||||
|
||||
return missingParams;
|
||||
}
|
||||
}
|
||||
|
||||
135
src/models/ConfigReader.tsx
Normal file
135
src/models/ConfigReader.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Env } from "./Env";
|
||||
import { EnvParam } from "./EnvParam";
|
||||
import { Config } from "./Config";
|
||||
|
||||
|
||||
/**
|
||||
* A utility class for parsing XML configuration files into a structured Config object.
|
||||
*
|
||||
* Supports both string-based and file-based parsing, extracting environment definitions
|
||||
* and their associated parameters. The expected XML format includes:
|
||||
* - Root element with tag "engine"
|
||||
* - Child elements "environment" with a "name" attribute
|
||||
* - Nested "parameter" elements with "name" and "value" attributes
|
||||
*
|
||||
* Provides validation and error handling for missing attributes.
|
||||
* Includes utility method to check if a file is in the supported XML format.
|
||||
*/
|
||||
export class ConfigReader {
|
||||
private readonly rootTag = "engine";
|
||||
|
||||
private readonly envTag = "environment";
|
||||
private readonly envNameAttr = "name";
|
||||
|
||||
private readonly paramTag = "parameter";
|
||||
private readonly paramNameAttr = "name";
|
||||
private readonly paramValAttr = "value";
|
||||
|
||||
private readonly templateTag = "template";
|
||||
|
||||
/**
|
||||
* Parses an XML string into a Config object.
|
||||
*
|
||||
* @param xmlString - The XML content as a string
|
||||
* @param fileType - The MIME type of the XML (default: 'application/xml')
|
||||
* @returns A Config object containing parsed environments and parameters, or null if parsing fails
|
||||
*/
|
||||
public parseFromString(xmlString: string, fileType: DOMParserSupportedType = 'application/xml'): Config | null {
|
||||
let parser = new DOMParser();
|
||||
let xml = parser.parseFromString(xmlString, fileType);
|
||||
this.checkTemplate(xml);
|
||||
let config = new Config();
|
||||
|
||||
let envs = this.parseEnvs(xml.querySelectorAll(`${this.rootTag}>${this.envTag}`));
|
||||
config.addEnvs(envs);
|
||||
|
||||
let tmplElement = xml.getElementsByTagName(this.templateTag)[0];
|
||||
let tmplText = tmplElement?.textContent?.trim();
|
||||
if (!tmplText) {
|
||||
throw new Error(`Template content is missing or empty in <${this.templateTag}> element.`);
|
||||
}
|
||||
|
||||
config.addTemplate(tmplText);
|
||||
|
||||
console.log("parsed from string res:", config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an XML file into a Config object asynchronously.
|
||||
*
|
||||
* @param file - The File object representing the XML file
|
||||
* @returns A Promise resolving to a Config object or null if parsing fails
|
||||
*/
|
||||
public async parseFromFile(file: File): Promise<Config | null> {
|
||||
let srcText = await file.text();
|
||||
return this.parseFromString(srcText, file.type as DOMParserSupportedType);
|
||||
}
|
||||
|
||||
private parseEnvs(xmlEnvs: NodeListOf<Element>): Env[] {
|
||||
let res: Env[] = [];
|
||||
let i = 0;
|
||||
for (let xml of xmlEnvs) {
|
||||
res.push(this.xmlToEnv(xml, i++));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private throwError(text: string): string {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
private xmlToEnv(xml: Element, id: number): Env {
|
||||
let name = xml.getAttribute(this.envNameAttr) ?? this.throwError(`no attr '${this.envNameAttr}' in '${xml.tagName}'`);
|
||||
let params = this.parseParams(xml);
|
||||
|
||||
return new Env(id, name, params);
|
||||
}
|
||||
|
||||
private parseParams(xml: Element): EnvParam[] {
|
||||
let paramElements = xml.getElementsByTagName(this.paramTag);
|
||||
let params: EnvParam[] = [];
|
||||
let id = 0;
|
||||
for (let p of paramElements) {
|
||||
params.push(this.xmlToParam(p, id++));
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
private xmlToParam(xmlParam: Element, id: number): EnvParam {
|
||||
let name = xmlParam.getAttribute(this.paramNameAttr) ?? this.throwError(`no attr '${this.paramNameAttr}' in '${this.paramTag}'`);
|
||||
let val = xmlParam.getAttribute(this.paramValAttr) ?? this.throwError(`no attr '${this.paramValAttr}' in '${this.paramTag}'`);
|
||||
|
||||
return new EnvParam(id, name, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file is in a supported format (text/xml).
|
||||
*
|
||||
* @param file - The File object to check
|
||||
* @returns True if the file is of type 'text/xml', otherwise returns an error message string
|
||||
*/
|
||||
public static isSupportedFormat(file: File): (boolean | string) {
|
||||
if (file.type !== "text/xml") {
|
||||
return `file format ${file.type} not supported (or extension is't .xml)`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public checkTemplate(xml: Document) {
|
||||
const templateElements = xml.getElementsByTagName(this.templateTag);
|
||||
|
||||
if (templateElements.length === 0) {
|
||||
this.throwError(`Missing required <${this.templateTag}> element in the XML.`);
|
||||
}
|
||||
|
||||
if(templateElements.length > 1) {
|
||||
this.throwError(`Multiple <${this.templateTag}> elements found. Only one is allowed.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
src/models/Env.ts
Normal file
60
src/models/Env.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { EnvParam } from "./EnvParam";
|
||||
import { NamedId } from "./NamedId";
|
||||
|
||||
|
||||
export class Env implements NamedId {
|
||||
constructor(
|
||||
public id?: number,
|
||||
public name?: string,
|
||||
public params: EnvParam[] = []
|
||||
) { }
|
||||
|
||||
public isDefault() {
|
||||
return this.name === "DEFAULT";
|
||||
}
|
||||
|
||||
addParams(payload: EnvParam): Env {
|
||||
payload.id = Math.random() * 10000;
|
||||
this.params.push(payload);
|
||||
return new Env(this.id, this.name, [...this.params]);
|
||||
}
|
||||
|
||||
delParam(param: EnvParam): Env {
|
||||
let idx = this.params.findIndex(el => el.id === param.id);
|
||||
if (idx > -1) {
|
||||
const newP = this.params.filter(el => el.id !== param.id);
|
||||
return new Env(this.id, this.name, newP);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public updateParams(param: EnvParam): Env {
|
||||
let idx = this.params.findIndex(el => el.id === param.id);
|
||||
if (idx > -1) {
|
||||
let newP = [...this.params];
|
||||
newP[idx] = param;
|
||||
return new Env(this.id, this.name, newP);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class AppEvent<T> {
|
||||
protected constructor(public payload: T) { }
|
||||
|
||||
public static add<T>(payload: T): AppEvent<T> {
|
||||
return new AddEvent(payload);
|
||||
}
|
||||
public static del<T>(payload: T): AppEvent<T> {
|
||||
return new DelEvent(payload);
|
||||
}
|
||||
public static update<T>(payload: T): AppEvent<T> {
|
||||
return new UpdateEvent(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddEvent<T> extends AppEvent<T> { }
|
||||
export class UpdateEvent<T> extends AppEvent<T> { }
|
||||
export class DelEvent<T> extends AppEvent<T> { }
|
||||
32
src/models/EnvParam.ts
Normal file
32
src/models/EnvParam.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NamedId } from "./NamedId";
|
||||
|
||||
export class EnvParam implements NamedId {
|
||||
constructor(
|
||||
public id?: number,
|
||||
public name?: string,
|
||||
public value?: string,
|
||||
public isChanged: boolean = false
|
||||
) { }
|
||||
|
||||
public Changed(v: boolean = true): EnvParam {
|
||||
return new EnvParam(
|
||||
this.id,
|
||||
this.name,
|
||||
this.value,
|
||||
v);
|
||||
}
|
||||
|
||||
public sanitize(v?: string): string {
|
||||
return v?.replace(/&/g, "&")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
?? "";
|
||||
}
|
||||
|
||||
public humanize(v?: string): string {
|
||||
return v ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
4
src/models/NamedId.ts
Normal file
4
src/models/NamedId.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface NamedId {
|
||||
id?: number;
|
||||
name?: string;
|
||||
}
|
||||
76
src/test/ConfigReader.test.ts
Normal file
76
src/test/ConfigReader.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import{test} from 'vitest'
|
||||
import { ConfigReader } from '../models/ConfigReader';
|
||||
|
||||
const cfgTemplate = `
|
||||
<engine>
|
||||
<environment name="DEFAULT">
|
||||
<parameter name = "host" value = "http://host1.xxx/api"/>
|
||||
<parameter name="MessageBrokerHosts" value="[ "smsk02ap432u:9096" ]" />
|
||||
</environment>
|
||||
<environment name="env1">
|
||||
<parameter name="port" value="60001"/>
|
||||
</environment>
|
||||
<environment name="env2">
|
||||
<parameter name="port" value="60002"/>
|
||||
<parameter name="MessageBrokerHosts" value="["smsk02ap430u:9096" , "smsk02ap433u:9096" ]" />
|
||||
</environment>
|
||||
|
||||
<template>
|
||||
{
|
||||
"Host": "@host@",
|
||||
"Port": @port@,
|
||||
|
||||
"ApiPath":"@host@:@port@/v1/data",
|
||||
|
||||
"MessageBroker":{
|
||||
"hosts":@MessageBrokerHosts@
|
||||
},
|
||||
|
||||
"basePath":"./@env_name@/in",
|
||||
|
||||
"NoParam:"@no_param@"
|
||||
}
|
||||
</template>
|
||||
</engine>
|
||||
`
|
||||
|
||||
|
||||
test("read from a file", async ({expect})=>{
|
||||
let sut = new ConfigReader();
|
||||
let file = new File([cfgTemplate],'cfg.json.xml',{type:'application/xml'});
|
||||
|
||||
// define a missing jsdom text() function,
|
||||
// that presents in the real DOM
|
||||
Object.defineProperty(file, 'text', {
|
||||
value: () => Promise.resolve(cfgTemplate),
|
||||
writable: true
|
||||
});
|
||||
|
||||
let cfg = await sut.parseFromFile(file);
|
||||
|
||||
expect(cfg).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test("load environments and params", ({expect})=>{
|
||||
let sut = new ConfigReader();
|
||||
|
||||
let cfg = sut.parseFromString(cfgTemplate);
|
||||
|
||||
expect(cfg?.envs).toHaveLength(3);
|
||||
expect(cfg?.envs.map(x=>x.name))
|
||||
.toEqual(expect.arrayContaining(["DEFAULT", "env1", "env2"]));
|
||||
expect(cfg?.envs.flatMap(x=>x.params))
|
||||
.toHaveLength(5);
|
||||
});
|
||||
|
||||
test("load template", ({expect})=>{
|
||||
let sut = new ConfigReader();
|
||||
|
||||
let cfg = sut.parseFromString(cfgTemplate);
|
||||
|
||||
expect(cfg?.template).toBeDefined();
|
||||
expect(cfg?.template.content.length).toBeGreaterThan(20);
|
||||
expect(cfg?.template.Params).toHaveLength(5)
|
||||
expect(cfg?.getTemplateAsJson()).not.toBeUndefined();
|
||||
expect(cfg?.validateParams()).does.has.length(1).and.contain("no_param");
|
||||
});
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
12
vitest.config.ts
Normal file
12
vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom"
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user