Compare commits

..

No commits in common. "c25d277abf9ae5c0d212f3c7a1887e060c3975aa" and "afbc630d48280439f1b6eb2a56794c9c050b15d6" have entirely different histories.

11 changed files with 145 additions and 1169 deletions

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/base": "^5.0.0-beta.70",
"@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"

View File

@ -1,17 +1,42 @@
.app-wrapper { #root {
display: flex; max-width: 1280px;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #121212;
padding: 2rem;
margin: 0 auto; margin: 0 auto;
max-width: 100%; padding: 2rem;
text-align: center;
} }
.title { .logo {
color: white; height: 6em;
text-align: center; padding: 1.5em;
margin-bottom: 2rem; will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
} }

View File

@ -1,16 +1,13 @@
import './App.css';
import './style/global.css';
import BackupForm from './components/BackupForm'; import BackupForm from './components/BackupForm';
import RestoreForm from './components/RestoreForm'; import RestoreForm from './components/RestoreForm';
function App() { function App() {
return ( return (
<div className="app-wrapper"> <div style={{ padding: "2rem" }}>
<h1 className="title">🚀 Snapix Dashboard</h1> <h1>🚀 Snapix Dashboard</h1>
<div className="form-sections"> <BackupForm />
<BackupForm /> <hr />
<RestoreForm /> <RestoreForm />
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,10 @@
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000', // שנה לכתובת האמיתית שלך
});
export const getNamespaces = () => api.get('/namespaces');
export const getPVCs = (namespace) => api.get(`/pvcs/${namespace}`);
export const createBackup = (payload) => api.post('/backup', payload);
export const restoreBackup = (payload) => api.post('/restore', payload);

View File

@ -1,74 +1,67 @@
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { getNamespaces, getPVCs, createBackup } from '../service'; import { getNamespaces, getPVCs, createBackup } from '../api/snapix';
import '../style/BackupForm.css';
import { Button, Input, Select, Option } from '@mui/base';
function BackupForm() { export default function BackupForm() {
const [namespaces, setNamespaces] = useState([]); const [namespaces, setNamespaces] = useState([]);
const [pvcs, setPvcs] = useState([]); const [pvcs, setPvcs] = useState([]);
const [namespace, setNamespace] = useState(''); const [selectedNs, setSelectedNs] = useState('');
const [pvc, setPvc] = useState(''); const [selectedPvc, setSelectedPvc] = useState('');
const [backupName, setBackupName] = useState(''); const [backupName, setBackupName] = useState('');
useEffect(() => { useEffect(() => {
getNamespaces().then(setNamespaces); getNamespaces().then(res => setNamespaces(res.data));
}, []); }, []);
useEffect(() => { useEffect(() => {
if (namespace) { if (selectedNs) {
getPVCs(namespace).then(setPvcs); getPVCs(selectedNs).then(res => setPvcs(res.data));
} else { } else {
setPvcs([]); setPvcs([]);
} }
}, [namespace]); }, [selectedNs]);
const handleSubmit = async () => { const handleSubmit = () => {
try { if (selectedNs && selectedPvc && backupName) {
await createBackup({ namespace, pvcName: pvc, backupName }); createBackup({
alert('✅ Backup created successfully!'); namespace: selectedNs,
} catch (error) { pvc_name: selectedPvc,
console.error(error); backup_name: backupName,
alert('❌ Backup failed'); }).then(() => {
alert("Backup started!");
setBackupName('');
});
} }
}; };
return ( return (
<div className="form-container BackupForm"> <div>
<h3>📦 Create Backup</h3> <h2>📦 Backup PVC</h2>
<div className="form-group">
<label>Namespace</label>
<Select value={namespace} onChange={(e) => setNamespace(e.target.value)}>
<Option value="">-- Select Namespace --</Option>
{namespaces.map((ns) => (
<Option key={ns} value={ns}>{ns}</Option>
))}
</Select>
</div>
<div className="form-group"> <label>Namespace:</label>
<label>PVC</label> <select onChange={e => setSelectedNs(e.target.value)} value={selectedNs}>
<Select value={pvc} onChange={(e) => setPvc(e.target.value)} disabled={!namespace}> <option value="">-- Select Namespace --</option>
<Option value="">-- Select PVC --</Option> {namespaces.map(ns => <option key={ns}>{ns}</option>)}
{pvcs.map((p) => ( </select>
<Option key={p} value={p}>{p}</Option>
))}
</Select>
</div>
<div className="form-group"> <label>PVC:</label>
<label>Backup Name</label> <select
<Input onChange={e => setSelectedPvc(e.target.value)}
type="text" value={selectedPvc}
value={backupName} disabled={!selectedNs}
onChange={(e) => setBackupName(e.target.value)} >
/> <option value="">-- Select PVC --</option>
</div> {pvcs.map(pvc => <option key={pvc}>{pvc}</option>)}
</select>
<Button className="btn" onClick={handleSubmit} disabled={!namespace || !pvc || !backupName}> <label>Backup Name:</label>
Create Backup <input
</Button> type="text"
value={backupName}
onChange={e => setBackupName(e.target.value)}
placeholder="e.g. git-bkp"
/>
<button onClick={handleSubmit}>Create Backup</button>
</div> </div>
); );
} }
export default BackupForm;

View File

@ -1,58 +1,49 @@
import { useState } from 'react'; import { useState } from 'react';
import { restoreBackup } from '../service'; import { restoreBackup } from '../api/snapix';
import '../style/RestoreForm.css';
import { Button, Input } from '@mui/base';
function RestoreForm() { export default function RestoreForm() {
const [namespace, setNamespace] = useState('');
const [targetPVC, setTargetPVC] = useState('');
const [backupName, setBackupName] = useState(''); const [backupName, setBackupName] = useState('');
const [targetNs, setTargetNs] = useState('');
const [targetPvc, setTargetPvc] = useState('');
const handleSubmit = async () => { const handleRestore = () => {
try { if (backupName && targetNs && targetPvc) {
await restoreBackup({ namespace, targetPVC, backupName }); restoreBackup({
alert('✅ Restore completed successfully!'); backup_name: backupName,
} catch (error) { target_namespace: targetNs,
console.error(error); target_pvc: targetPvc,
alert('❌ Restore failed'); }).then(() => {
alert("Restore started!");
});
} }
}; };
return ( return (
<div className="form-container RestoreForm"> <div>
<h3> Restore from Backup</h3> <h2>🔁 Restore PVC</h2>
<div className="form-group">
<label>Namespace</label>
<input
type="text"
value={namespace}
onChange={(e) => setNamespace(e.target.value)}
/>
</div>
<div className="form-group"> <label>Backup Name:</label>
<label>Target PVC</label> <input
<input type="text"
type="text" value={backupName}
value={targetPVC} onChange={e => setBackupName(e.target.value)}
onChange={(e) => setTargetPVC(e.target.value)} />
/>
</div>
<div className="form-group"> <label>Target Namespace:</label>
<label>Backup Name</label> <input
<Input type="text"
type="text" value={targetNs}
value={backupName} onChange={e => setTargetNs(e.target.value)}
onChange={(e) => setBackupName(e.target.value)} />
/>
</div>
<Button className="btn" onClick={handleSubmit} disabled={!namespace || !targetPVC || !backupName}> <label>Target PVC:</label>
Restore <input
</Button> type="text"
value={targetPvc}
onChange={e => setTargetPvc(e.target.value)}
/>
<button onClick={handleRestore}>Restore</button>
</div> </div>
); );
} }
export default RestoreForm;

View File

@ -1,33 +0,0 @@
const API_BASE = "http://localhost:8000";
export async function getNamespaces() {
const res = await fetch(`${API_BASE}/namespaces`);
if (!res.ok) throw new Error("Failed to fetch namespaces");
return await res.json();
}
export async function getPVCs(namespace) {
const res = await fetch(`${API_BASE}/pvcs/${namespace}`);
if (!res.ok) throw new Error("Failed to fetch PVCs");
return await res.json();
}
export async function createBackup({ namespace, pvcName, backupName }) {
const res = await fetch(`${API_BASE}/backup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ namespace, pvc_name: pvcName, backup_name: backupName }),
});
if (!res.ok) throw new Error("Failed to create backup");
return await res.json();
}
export async function restoreBackup({ namespace, targetPVC, backupName }) {
const res = await fetch(`${API_BASE}/restore`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ namespace, target_pvc: targetPVC, backup_name: backupName }),
});
if (!res.ok) throw new Error("Failed to restore backup");
return await res.json();
}

View File

@ -1,7 +0,0 @@
.BackupForm {
background: #e3f2fd;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
color: rgb(0, 0, 0);
}

View File

@ -1,7 +0,0 @@
.RestoreForm {
background: #f1f8e9;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
color: rgb(0, 0, 0);
}

View File

@ -1,58 +0,0 @@
/* Global button style */
.btn {
padding: 10px;
font-weight: bold;
background: #1976d2;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Inputs */
input, select {
padding: 8px;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
/* Shared spacing for forms */
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 4px;
font-weight: 500;
color: #333;
}
.form-sections {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 2rem;
margin: 0 auto;
}
.form-container {
display: flex;
flex-direction: column;
}
/* .input {
padding: 8px;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
background-color: transparent;
} */