feat: add config download functionality
This commit is contained in:
@@ -107,7 +107,7 @@ function App() {
|
||||
AppState.Instance.loadConfig(x);
|
||||
setEnvs(x.envs);
|
||||
setConfig(x);
|
||||
}} />
|
||||
}} config={config} />
|
||||
</div>
|
||||
{envs.length > 0 ?
|
||||
(<div className="row">
|
||||
|
||||
39
src/builders/ConfigBuilder.ts
Normal file
39
src/builders/ConfigBuilder.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Config } from "../models/Config";
|
||||
import { EnvBuilder } from "./EnvBuilder";
|
||||
|
||||
export class ConfigBuilder {
|
||||
/**
|
||||
* Builds a full XML config file with all environments and template.
|
||||
*/
|
||||
public static buildFullXml(config: Config): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("<engine>");
|
||||
|
||||
// Add all environments
|
||||
for (const env of config.envs) {
|
||||
const envXml = new EnvBuilder().buildEnv(env);
|
||||
lines.push(envXml);
|
||||
}
|
||||
|
||||
// Add template
|
||||
lines.push(" <template>");
|
||||
lines.push(config.template.content);
|
||||
lines.push(" </template>");
|
||||
|
||||
lines.push("</engine>");
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates filename with timestamp: config_yy-dd-MM-HHmm.json.xml
|
||||
*/
|
||||
public static generateFilename(): string {
|
||||
const now = new Date();
|
||||
const yy = now.getFullYear().toString().slice(-2);
|
||||
const dd = String(now.getDate()).padStart(2, '0');
|
||||
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const HH = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
return `config_${yy}-${dd}-${MM}-${HH}${mm}.json.xml`;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,14 @@ export class EnvBuilder implements IBuilder<Env> {
|
||||
this._src = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds XML for a single environment (static method for direct use)
|
||||
*/
|
||||
public buildEnv(env: Env): string {
|
||||
this.src = env;
|
||||
return this.build();
|
||||
}
|
||||
|
||||
build(): string {
|
||||
return this
|
||||
.open()
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Env } from "../models/Env";
|
||||
import { EnvBuilder } from "./EnvBuilder";
|
||||
|
||||
|
||||
|
||||
|
||||
import { ConfigBuilder } from "./ConfigBuilder";
|
||||
|
||||
export interface IBuilder<T> {
|
||||
get src(): T;
|
||||
@@ -20,7 +17,9 @@ export class Builder {
|
||||
};
|
||||
|
||||
public static getEnvs(envs: Env[]): string {
|
||||
return envs.map(x => Builder.getEnv(x).build()).join("\r\n");
|
||||
return envs.map(x => Builder.getEnv(x).build()).join("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
export { ConfigBuilder };
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Env } from "../models/Env";
|
||||
import { ConfigReader } from "../models/ConfigReader";
|
||||
import { Config } from "../models/Config";
|
||||
import { ConfigBuilder } from "../builders/ConfigBuilder";
|
||||
|
||||
|
||||
export function FileChooser(props: { onSelected: (x: Config) => void }) {
|
||||
export function FileChooser(props: { onSelected: (x: Config) => void, config?: Config }) {
|
||||
async function handleFile(x: React.ChangeEvent<HTMLInputElement>) {
|
||||
let file = x.target.files![0];
|
||||
|
||||
@@ -23,11 +23,42 @@ export function FileChooser(props: { onSelected: (x: Config) => void }) {
|
||||
props.onSelected(cfg);
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
if (!props.config) {
|
||||
alert("No configuration loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
const xmlContent = ConfigBuilder.buildFullXml(props.config);
|
||||
const filename = ConfigBuilder.generateFilename();
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([xmlContent], { type: "text/xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-2">
|
||||
<button className="btn btn-primary" onClick={handleNew} >Create new</button>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleDownload}
|
||||
disabled={!props.config}
|
||||
title="Download full config template"
|
||||
>
|
||||
⬇ Download
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-1">or</div>
|
||||
|
||||
<div className="col">
|
||||
|
||||
11
src/test/ConfigBuilder.test.ts
Normal file
11
src/test/ConfigBuilder.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ConfigBuilder } from '../builders/ConfigBuilder';
|
||||
|
||||
describe('ConfigBuilder', () => {
|
||||
describe('generateFilename', () => {
|
||||
it('should generate filename with correct format', () => {
|
||||
const filename = ConfigBuilder.generateFilename();
|
||||
expect(filename).toMatch(/^config_\d{2}-\d{2}-\d{2}-\d{4}\.json\.xml$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user