Build EnvHelper desktop app
Some checks failed
Build Windows App / build-windows (push) Has been cancelled
Some checks failed
Build Windows App / build-windows (push) Has been cancelled
This commit is contained in:
150
src/App.tsx
Normal file
150
src/App.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Check, Clipboard, FileDown, FileInput, RefreshCcw, ShieldCheck, Sparkles } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { transformEnv } from "./env";
|
||||
|
||||
const sampleEnv = `APP_IMAGE=git.wilkensxl.de/mrsphay/hilden-directory-gateway:latest
|
||||
APP_PORT=3000
|
||||
|
||||
NODE_ENV=production
|
||||
PUBLIC_BASE_URL=https://gateway.example.local
|
||||
TRUSTED_IFRAME_ANCESTORS=https://www.hilden.de
|
||||
|
||||
DATABASE_URL=postgresql://hilden_app:CHANGE_ME_POSTGRES_PASSWORD@postgres:5432/hilden_directory
|
||||
POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD
|
||||
|
||||
SESSION_SECRET=CHANGE_ME_AT_LEAST_32_RANDOM_CHARACTERS
|
||||
ENCRYPTION_KEY_BASE64=CHANGE_ME_32_RANDOM_BYTES_AS_BASE64
|
||||
|
||||
BOOTSTRAP_ADMIN_EMAIL=admin@example.local
|
||||
BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_LONG_INITIAL_ADMIN_PASSWORD`;
|
||||
|
||||
export default function App() {
|
||||
const [input, setInput] = useState(sampleEnv);
|
||||
const [loadedPath, setLoadedPath] = useState<string | null>(null);
|
||||
const [copyLabel, setCopyLabel] = useState("Kopieren");
|
||||
const [generation, setGeneration] = useState(0);
|
||||
const result = useMemo(() => transformEnv(input), [input, generation]);
|
||||
|
||||
async function openFile() {
|
||||
const file = await window.envHelper?.openFile();
|
||||
if (file) {
|
||||
setInput(file.content);
|
||||
setLoadedPath(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
await window.envHelper?.saveFile(result.output);
|
||||
}
|
||||
|
||||
async function copyOutput() {
|
||||
await navigator.clipboard.writeText(result.output);
|
||||
setCopyLabel("Kopiert");
|
||||
window.setTimeout(() => setCopyLabel("Kopieren"), 1400);
|
||||
}
|
||||
|
||||
function regenerate() {
|
||||
setGeneration((current) => current + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="shell">
|
||||
<section className="hero">
|
||||
<div>
|
||||
<p className="eyebrow"><Sparkles size={16} /> EnvHelper</p>
|
||||
<h1>.env Platzhalter sauber ersetzen</h1>
|
||||
<p className="subline">
|
||||
Erkennt CHANGE_ME Werte, erzeugt passende Secrets im richtigen Format und hält gleiche Platzhalter synchron.
|
||||
</p>
|
||||
</div>
|
||||
<div className="heroActions">
|
||||
<button className="secondary" onClick={openFile} type="button">
|
||||
<FileInput size={18} /> Datei laden
|
||||
</button>
|
||||
<button className="secondary" onClick={saveFile} type="button" disabled={!result.output}>
|
||||
<FileDown size={18} /> .env speichern
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="stats">
|
||||
<div>
|
||||
<span>{result.changedCount}</span>
|
||||
<p>Fundstellen</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{result.replacements.length}</span>
|
||||
<p>eindeutige Werte</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{loadedPath ? "Datei" : "Text"}</span>
|
||||
<p>Quelle</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="workspace">
|
||||
<div className="panel">
|
||||
<div className="panelHeader">
|
||||
<div>
|
||||
<h2>Input</h2>
|
||||
<p>{loadedPath ?? "Direkt einfügen oder Beispieldaten ersetzen"}</p>
|
||||
</div>
|
||||
<button className="iconButton" onClick={() => setInput(sampleEnv)} title="Beispiel laden" type="button">
|
||||
<RefreshCcw size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
spellCheck={false}
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
aria-label="Env input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panelHeader">
|
||||
<div>
|
||||
<h2>Output</h2>
|
||||
<p>Bereit für Deployment oder Secret Store</p>
|
||||
</div>
|
||||
<div className="buttonRow">
|
||||
<button className="iconButton" onClick={regenerate} title="Neu erzeugen" type="button">
|
||||
<RefreshCcw size={18} />
|
||||
</button>
|
||||
<button className="primary" onClick={copyOutput} type="button">
|
||||
{copyLabel === "Kopiert" ? <Check size={18} /> : <Clipboard size={18} />} {copyLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea spellCheck={false} value={result.output} readOnly aria-label="Env output" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="replacementPanel">
|
||||
<div className="panelHeader compact">
|
||||
<div>
|
||||
<h2>Erkannte Anforderungen</h2>
|
||||
<p>Formate werden aus Variablennamen und Platzhaltertext abgeleitet.</p>
|
||||
</div>
|
||||
<ShieldCheck size={22} />
|
||||
</div>
|
||||
|
||||
{result.replacements.length === 0 ? (
|
||||
<div className="empty">Keine CHANGE_ME Platzhalter gefunden.</div>
|
||||
) : (
|
||||
<div className="replacementGrid">
|
||||
{result.replacements.map((replacement) => (
|
||||
<article className="replacement" key={replacement.placeholder}>
|
||||
<div>
|
||||
<strong>{replacement.placeholder}</strong>
|
||||
<p>{replacement.keys.join(", ")}</p>
|
||||
</div>
|
||||
<span>{replacement.format}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user