refactor: complete application rewrite with modern UI

This commit is contained in:
sokol
2026-02-19 22:55:26 +03:00
parent 271b530fa1
commit a6cc5a9827
26 changed files with 3036 additions and 794 deletions

View File

@@ -1,125 +1,153 @@
import { useState, useEffect } from "react";
import { AddEvent, AppEvent, DelEvent, Env, UpdateEvent } from "../../models/Env";
import { EnvParam } from "../../models/EnvParam";
import { EnvironmentParam } from "./EnvironmentParam";
import { useState, useEffect } from 'react';
import { Plus, Minus } from 'lucide-react';
import { Button, Select, Card, CardBody } from '../../components/ui';
import { Env, AddEvent, RemoveEvent, 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, onAdd: (env: Env) => number, onRemove: (envId: number) => void }) {
const [currEnvId, setCurrEnvId] = useState(props.envs[0]?.id);
// Sync currEnvId when props.envs changes
useEffect(() => {
if (!props.envs.find(e => e.id === currEnvId)) {
setCurrEnvId(props.envs[0]?.id);
}
}, [props.envs, currEnvId]);
const currEnv = props.envs.find(e => e.id === currEnvId) ?? 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) {
props.onChanged(env);
setCurrEnvId(env.id);
}
}
function handleAddEnv() {
const name = prompt("Enter new environment name:");
if (!name || name.trim() === "") return;
// Calculate next integer ID based on max existing ID
const maxId = props.envs.reduce((max, e) => Math.max(max, e.id ?? 0), -1);
const newId = maxId + 1;
const newEnv = new Env(
newId,
name.trim(),
[...currEnv.params]
);
// Parent synchronously adds the env and returns the index
const newIdx = props.onAdd(newEnv);
setCurrEnvId(newEnv.id);
props.onSelected(newIdx);
}
function handleRemoveEnv() {
if (currEnv.isDefault()) {
alert("Cannot remove DEFAULT environment");
return;
}
if (!confirm(`Remove environment "${currEnv.name}"?`)) return;
const idx = props.envs.findIndex(x => x.id === currEnv.id);
if (idx > -1 && currEnv.id !== undefined) {
// Let parent handle the removal
props.onRemove(currEnv.id);
const newIdx = Math.max(0, idx - 1);
const newEnv = props.envs[newIdx];
if (newEnv?.id !== undefined) {
setCurrEnvId(newEnv.id);
}
props.onSelected(newIdx);
}
}
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 g-0">
<div className="col">
<select
id="environments"
name="environments"
aria-label="Environments"
className="form-select"
value={currEnvId}
onChange={x => {
let id = Number.parseInt(x.target.value);
setCurrEnvId(id);
props.onSelected(id);
}}>
{selectOptions}
</select>
</div>
<div className="col-auto ms-2">
<button className="btn btn-success" onClick={handleAddEnv} title="Add environment"></button>
</div>
<div className="col-auto ms-2">
<button className="btn btn-danger" onClick={handleRemoveEnv} title="Remove environment" disabled={currEnv.isDefault()}></button>
</div>
</div>
<div className="row">Params</div>
{paramCtrls}
<EnvironmentParam key={`${currEnv.id}-new`}
param={new EnvParam(-1, "", "")}
onChanged={handleParamChanged}
isNew={true}
/>
</>
);
interface EnvironmentProps {
envs: Env[];
onChanged: (env: Env) => void;
onSelected: (envId: number) => void;
onAdd: (env: Env) => number;
onRemove: (envId: number) => void;
}
export function Environment({ envs, onChanged, onSelected, onAdd, onRemove }: EnvironmentProps) {
const [currEnvId, setCurrEnvId] = useState<number>(envs[0]?.id ?? 0);
// Sync currEnvId when envs changes
useEffect(() => {
if (!envs.find(e => e.id === currEnvId)) {
setCurrEnvId(envs[0]?.id ?? 0);
}
}, [envs, currEnvId]);
const currEnv = envs.find(e => e.id === currEnvId) ?? envs[0];
function handleParamChanged(event: AddEvent<EnvParam> | RemoveEvent<EnvParam> | UpdateEvent<EnvParam>) {
let newEnv: Env = currEnv;
let isChanged = false;
if (event instanceof RemoveEvent) {
newEnv = currEnv.delParam(event.payload);
isChanged = true;
} else if (event instanceof AddEvent) {
newEnv = currEnv.addParams(event.payload);
isChanged = true;
} else if (event instanceof UpdateEvent) {
newEnv = currEnv.updateParams(event.payload);
isChanged = true;
}
if (isChanged) {
onChanged(newEnv);
setCurrEnvId(newEnv.id ?? 0);
}
}
function handleAddEnv() {
const name = prompt('Enter new environment name:');
if (!name || name.trim() === '') return;
// Calculate next integer ID based on max existing ID
const maxId = envs.reduce((max, e) => Math.max(max, e.id ?? 0), -1);
const newId = maxId + 1;
const newEnv = new Env(newId, name.trim(), [...currEnv.params]);
const newIdx = onAdd(newEnv);
setCurrEnvId(newEnv.id ?? 0);
onSelected(newIdx);
}
function handleRemoveEnv() {
if (currEnv.isDefault()) {
alert('Cannot remove DEFAULT environment');
return;
}
if (!confirm(`Remove environment "${currEnv.name}"?`)) return;
const idx = envs.findIndex(x => x.id === currEnv.id);
if (idx > -1 && currEnv.id !== undefined) {
onRemove(currEnv.id);
const newIdx = Math.max(0, idx - 1);
const newEnv = envs[newIdx];
if (newEnv?.id !== undefined) {
setCurrEnvId(newEnv.id);
}
onSelected(newIdx);
}
}
const selectOptions = envs.map((x) => ({
value: x.id ?? 0,
label: x.name ?? 'Unknown',
}));
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 (
<Card variant="bordered" padding="none" className="h-full">
<CardBody className="space-y-4">
{/* Environment Selector */}
<div className="flex items-center gap-2">
<div className="flex-1">
<Select
label="Environment"
value={currEnvId}
options={selectOptions}
onChange={(e) => {
const id = Number.parseInt(e.target.value);
setCurrEnvId(id);
onSelected(id);
}}
id="environments"
/>
</div>
<Button
variant="success"
size="sm"
onClick={handleAddEnv}
title="Add environment"
icon={Plus}
/>
<Button
variant="danger"
size="sm"
onClick={handleRemoveEnv}
title="Remove environment"
icon={Minus}
disabled={currEnv.isDefault()}
/>
</div>
{/* Parameters Section */}
<div>
<h3 className="text-sm font-semibold text-slate-700 mb-3 uppercase tracking-wide">
Parameters
</h3>
<div className="space-y-2">
{paramCtrls}
<EnvironmentParam
key={`${currEnv.id}-new`}
param={new EnvParam(-1, '', '')}
onChanged={handleParamChanged}
isNew={true}
/>
</div>
</div>
</CardBody>
</Card>
);
}