Add toast alerts and fix bugs
This commit is contained in:
parent
0945e7c7c2
commit
c0c3c92b55
Binary file not shown.
@ -35,3 +35,7 @@ sections:
|
||||
name: Vault
|
||||
url: https://vault.dvirlabs.com
|
||||
name: Dev-tools
|
||||
- apps: []
|
||||
name: fgbhn
|
||||
- apps: []
|
||||
name: dfgb
|
||||
|
||||
@ -20,7 +20,6 @@ app.add_middleware(
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# ENV
|
||||
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
|
||||
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY")
|
||||
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY")
|
||||
@ -34,12 +33,13 @@ minio_client = Minio(
|
||||
)
|
||||
|
||||
BUCKET = MINIO_BUCKET or "navix-icons"
|
||||
APPS_FILE = Path(__file__).parent / "apps.yaml"
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def root():
|
||||
return {"message": "Welcome to the FastAPI application!"}
|
||||
|
||||
APPS_FILE = Path(__file__).parent / "apps.yaml"
|
||||
|
||||
@router.get("/apps")
|
||||
def get_apps():
|
||||
@ -48,15 +48,19 @@ def get_apps():
|
||||
with open(APPS_FILE, "r") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
class AppData(BaseModel):
|
||||
name: str
|
||||
icon: str
|
||||
description: str
|
||||
url: str
|
||||
|
||||
|
||||
class AppEntry(BaseModel):
|
||||
section: str
|
||||
app: AppData
|
||||
original_name: str | None = None
|
||||
|
||||
|
||||
@router.post("/add_app")
|
||||
def add_app(entry: AppEntry):
|
||||
@ -81,6 +85,7 @@ def add_app(entry: AppEntry):
|
||||
|
||||
return {"status": "added"}
|
||||
|
||||
|
||||
@router.post("/edit_app")
|
||||
def edit_app(entry: AppEntry):
|
||||
if not APPS_FILE.exists():
|
||||
@ -93,20 +98,21 @@ def edit_app(entry: AppEntry):
|
||||
for section in current["sections"]:
|
||||
if section["name"] == entry.section:
|
||||
for i, app in enumerate(section["apps"]):
|
||||
if app["name"] == entry.app.name:
|
||||
if app["name"] == (entry.original_name or entry.app.name):
|
||||
section["apps"][i] = entry.app.dict()
|
||||
updated = True
|
||||
break
|
||||
break
|
||||
|
||||
if not updated:
|
||||
return {"error": "App not found to edit"}, 404
|
||||
return JSONResponse(status_code=404, content={"error": "App not found to edit"})
|
||||
|
||||
with open(APPS_FILE, "w") as f:
|
||||
yaml.safe_dump(current, f)
|
||||
|
||||
return {"status": "updated"}
|
||||
|
||||
|
||||
@router.post("/delete_app")
|
||||
def delete_app(entry: AppEntry):
|
||||
if not APPS_FILE.exists():
|
||||
@ -125,7 +131,7 @@ def delete_app(entry: AppEntry):
|
||||
break
|
||||
|
||||
if not deleted:
|
||||
return {"error": "App not found to delete"}, 404
|
||||
return JSONResponse(status_code=404, content={"error": "App not found to delete"})
|
||||
|
||||
with open(APPS_FILE, "w") as f:
|
||||
yaml.safe_dump(current, f)
|
||||
@ -138,10 +144,11 @@ def get_public_icon_url(filename: str):
|
||||
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
||||
return JSONResponse(content={"url": url})
|
||||
|
||||
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
|
||||
# ✅ This is the missing part:
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
|
||||
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
@ -11,7 +11,8 @@
|
||||
"react": "^19.1.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"react-toastify": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
@ -1488,6 +1489,14 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -2440,6 +2449,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
"react": "^19.1.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.5.0",
|
||||
"react-toastify": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
|
||||
@ -4,8 +4,15 @@ import SectionGrid from './components/SectionGrid';
|
||||
import AppModal from './components/AppModal';
|
||||
import ConfirmDialog from './components/ConfirmDialog';
|
||||
import Clock from './components/Clock';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { IoIosAdd } from 'react-icons/io';
|
||||
import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api';
|
||||
import {
|
||||
fetchSections,
|
||||
addAppToSection,
|
||||
editAppInSection,
|
||||
deleteAppFromSection,
|
||||
} from './services/api';
|
||||
|
||||
function App() {
|
||||
const [sections, setSections] = useState([]);
|
||||
@ -15,7 +22,10 @@ function App() {
|
||||
|
||||
const loadSections = () => {
|
||||
fetchSections()
|
||||
.then(data => setSections(data.sections || []))
|
||||
.then(data => {
|
||||
const filtered = (data.sections || []).filter(section => (section.apps?.length ?? 0) > 0);
|
||||
setSections(filtered);
|
||||
})
|
||||
.catch(err => console.error('Failed to fetch sections:', err));
|
||||
};
|
||||
|
||||
@ -26,7 +36,7 @@ function App() {
|
||||
const handleDelete = (app) => {
|
||||
setConfirmData({
|
||||
message: `Are you sure you want to delete "${app.name}"?`,
|
||||
app
|
||||
app,
|
||||
});
|
||||
};
|
||||
|
||||
@ -34,16 +44,48 @@ function App() {
|
||||
if (!confirmData?.app) return;
|
||||
|
||||
try {
|
||||
await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app });
|
||||
await deleteAppFromSection({
|
||||
section: confirmData.app.section,
|
||||
app: confirmData.app,
|
||||
});
|
||||
toast.error(`App "${confirmData.app.name}" deleted successfully!`);
|
||||
loadSections();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete app:', err);
|
||||
alert('Failed to delete app');
|
||||
toast.error('Failed to delete app');
|
||||
} finally {
|
||||
setConfirmData(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSubmit = async (data) => {
|
||||
try {
|
||||
await addAppToSection(data);
|
||||
toast.success(`App "${data.app.name}" added successfully!`);
|
||||
setShowAdd(false);
|
||||
loadSections();
|
||||
} catch (err) {
|
||||
console.error('Failed to add app:', err);
|
||||
toast.error('Failed to add app');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (data) => {
|
||||
try {
|
||||
await editAppInSection({
|
||||
section: data.section,
|
||||
app: data.app,
|
||||
original_name: data.original_name || data.app.name,
|
||||
});
|
||||
toast.warning(`App "${data.app.name}" updated successfully!`);
|
||||
setEditData(null);
|
||||
loadSections();
|
||||
} catch (err) {
|
||||
console.error('Failed to update app:', err);
|
||||
toast.error('Failed to update app');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Clock />
|
||||
@ -55,11 +97,8 @@ function App() {
|
||||
{showAdd && (
|
||||
<AppModal
|
||||
mode="add"
|
||||
onSubmit={addAppToSection}
|
||||
onClose={() => {
|
||||
setShowAdd(false);
|
||||
loadSections();
|
||||
}}
|
||||
onSubmit={handleAddSubmit}
|
||||
onClose={() => setShowAdd(false)}
|
||||
sections={sections}
|
||||
/>
|
||||
)}
|
||||
@ -68,11 +107,8 @@ function App() {
|
||||
<AppModal
|
||||
mode="edit"
|
||||
initialData={editData}
|
||||
onSubmit={editAppInSection}
|
||||
onClose={() => {
|
||||
setEditData(null);
|
||||
loadSections();
|
||||
}}
|
||||
onSubmit={handleEditSubmit}
|
||||
onClose={() => setEditData(null)}
|
||||
sections={sections}
|
||||
/>
|
||||
)}
|
||||
@ -85,16 +121,30 @@ function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{sections.map(section => (
|
||||
{sections.map((section) => (
|
||||
<SectionGrid
|
||||
key={section.name}
|
||||
section={section}
|
||||
onEdit={setEditData}
|
||||
onEdit={(app) =>
|
||||
setEditData({ ...app, section: section.name, original_name: app.name })
|
||||
}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
|
||||
<IoIosAdd className="add-button" onClick={() => setShowAdd(true)} />
|
||||
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={3000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="colored"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import '../style/AddAppModal.css';
|
||||
|
||||
function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) {
|
||||
@ -12,7 +12,24 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
await onSubmit({ section, app: { name, icon, description, url } });
|
||||
|
||||
const appData = {
|
||||
name,
|
||||
icon,
|
||||
description,
|
||||
url,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
section,
|
||||
app: appData,
|
||||
};
|
||||
|
||||
if (mode === 'edit') {
|
||||
payload.original_name = initialData.original_name || initialData.name;
|
||||
}
|
||||
|
||||
await onSubmit(payload);
|
||||
setOpen(false);
|
||||
if (onClose) onClose();
|
||||
};
|
||||
@ -40,8 +57,10 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
|
||||
disabled={mode === 'edit'}
|
||||
>
|
||||
<option value="" disabled>Select a section</option>
|
||||
{sections.map(s => (
|
||||
<option key={s.name} value={s.name}>{s.name}</option>
|
||||
{sections.map((s) => (
|
||||
<option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
<option value="__new__">➕ Create new section...</option>
|
||||
</select>
|
||||
@ -51,15 +70,37 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
|
||||
type="text"
|
||||
placeholder="New section name"
|
||||
value={section}
|
||||
onChange={e => setSection(e.target.value)}
|
||||
onChange={(e) => setSection(e.target.value)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required />
|
||||
<input type="text" placeholder="Icon name (e.g. grafana.svg) or full URL" value={icon} onChange={e => setIcon(e.target.value)} />
|
||||
<input type="text" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} />
|
||||
<input type="text" placeholder="App URL" value={url} onChange={e => setUrl(e.target.value)} required />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="App name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Icon name (e.g. grafana.svg) or full URL"
|
||||
value={icon}
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="App URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button type="submit">{mode === 'edit' ? 'Update' : 'Add'}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -23,11 +23,11 @@ export async function getIconUrl(filename) {
|
||||
return data.url;
|
||||
}
|
||||
|
||||
export async function editAppInSection({ section, app }) {
|
||||
export async function editAppInSection({ section, app, original_name }) {
|
||||
const res = await fetch(`${API_BASE}/edit_app`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ section, app })
|
||||
body: JSON.stringify({ section, app, original_name })
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user