From be3f9aa03b0495a3155989f429e996c3afe2ef2d Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 4 Jul 2025 06:26:12 +0300 Subject: [PATCH 1/2] Add edit and delete buttons --- backend/__pycache__/main.cpython-313.pyc | Bin 4499 -> 7087 bytes backend/main.py | 52 +++++++++++++ frontend/.gitignore | 3 + frontend/src/App.jsx | 84 ++++++++++++++++++-- frontend/src/components/AddAppModal.jsx | 91 ---------------------- frontend/src/components/AppCard.jsx | 44 ++++++----- frontend/src/components/AppGrid.jsx | 6 +- frontend/src/components/AppModal.jsx | 70 +++++++++++++++++ frontend/src/components/ConfirmDialog.jsx | 18 +++++ frontend/src/components/SectionGrid.jsx | 4 +- frontend/src/services/api.js | 22 ++++++ frontend/src/style/AppCard.css | 28 +++++++ 12 files changed, 300 insertions(+), 122 deletions(-) delete mode 100644 frontend/src/components/AddAppModal.jsx create mode 100644 frontend/src/components/AppModal.jsx create mode 100644 frontend/src/components/ConfirmDialog.jsx diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 94945ccac38ec6251befaadf2bdef6f8c9b321dc..f183f0bbe10265dc56e4f9ea4553901dca328e32 100644 GIT binary patch delta 2604 zcmcguUu;uV7(eIU+upXfUAwk0+iu|Q5R#G1b=Rx)1ND7-NLpjJ%eAs3*U4tNzLgWv2sgq>Ylz4y7KzQ7La|u< z&|qARjzksm2^%6-?oI30an#Xfznl}2uPbtOlK!tS1AxvUdzgE_2$e_VlVJXs=d-?#wKzpv5 zykc-*8#!!nwiToGYv@kPI)xQcFC##j#lD}tpS~6tP~eSF;OiNZ(3ClZV)_hH@III? zv_3nnjH*4J;xydd!*^?xO^ujjH08{WYs4>jLU}_qYG*6aFqTSosBQ?RR#Fftv;%ai z^>p|Yz|O;~o=GG`B|at&$H$ehI2IS_qxkt0C`8>tX-@;>`0+$IG!_X<0+|vjC-f0z zd^DnhMQx*;e7p`i3Ia&Hx`~ec6t+=#mO{Rka5V3cq?#8CEnj{)U9kex9+=t*SJDYN z97T5p`z^sWBe-q}6*EFb)>4)s6I$KGUA^h8=(O#Iz9L}IW02Vk9K8k&gr%rHvc4_6|AWZC$~&T&W@fQy(!e+aeC8TA9kMW z{m!|5N}Ix2+v?Q8sZD9$dFh;VzTsTMOc>$cr7uSc%tc>^+8=gr7o^P3eJZ8TX&W5&Gpa#O~Nt17k;&3!0h#5{? ztbzP)>MQOoiZFm%fm}_iQcJV5H_?+n`8ng$$-GT2*rn&q&4v< zX&qXUmIw5yeV%#FhY;0Jl2NoYAl2667wy-yfqw|3xl(_Br#sUFx+7m3Jf43fwbC8m z7UY$LL!Bl!OlCB$SLA;!y|TYZukb|G?Qo|n#g{jew*~J68;d9_B==~w?V#>?wHXWV zj8JS4{Bk(-SJ@esi%9JQKLxI2_5US2mwWn9`rzq_Syy1PCtJ7nvj1XW*1h*)ApOFp z2S3~Ujqkd2O}gH2tzo8p&)EYP0yFNtlbgFzohN(mn60Un=?zm`X3gI8ky&$7*6B|1 zw~eLqCMrHlQGDw%=BCSX#=J43+qfh?xhOtgL43deT7oKH#EX_kN)W1+O3==JVHATt z?H7$&z%SR91?$+Wc0;g=zglMqZX%ND*uK?t-x~lVoa2Y$%2-4hlUNl#cTi5dwZEf# z+fJ2=Q({#5p^&F0pO-?~g@?2BQ#Qa9oY4yMZuC_>v}gC2uO}!B3LO zl8>IGK~*XuwUmMtAZHwnD$)4hP%H`ytGlR)g@|f788SCp=&_oA#A*{A?Ivf;HQonb z<2l{t(Rg?~7I{id0EZqb$)Cti=8jcxBKqjD=$q3cY@0_K%*>aN4$DH&^@eM5-z;{_ zF=g13+P;9GTVN66%p2y`Ax3wCa$cInt~q8ka9&+N&{0l#-Y~b0tSfzqqhBI_0v;nY Apa1{> delta 750 zcmZ9KO=}ZD7{_OlZL)dOO}EJ=o0i6=X|hO7pb4d=DT<(AB85yV78gTILfo=>VK$-Q zNl_GfGCUXY;-Pr)poe|{zd{cl!X8BM3mBs4(V4VSbYOlv&pglnc^US@IwaELK){RG zGB?W)^B#o0a$~y0^TYVNV~~Cu@Dfi3PquGkW-!!z{yaX5pdrZO1Z1!ZE$jt?RpsIz zUqqLdkRH;*xWA^;vR>ENwXA zNKcc*=3RuU5rhaLLQ$idu9bH|!XbH?B*_Z#lGKI}M@^wAXlXcdwwHG`!46bSvvXMK zP@`$IEjxH-)@Y;NX!ne&38GMi1L25G@k1yrEU@A!wyg^ckI@%@=v3J{^%YKq0*%4_ znVnM`IjZNVO?) zFfYb&3<~1@+$OuU-BzR4GdgYC$?{Sa-ievaf8up6=HLR$&+q~b1Jcvd&?7xB(b$RT zx$2ZB4Z5rKI&YBPU{TvA_xvdgzrs%<3Nz^y1`XTpTBqG(Z=pG<1Or9R-kdru2Rp^% zla_6n&)V%jFpXNpV3JConzYCQ_TZDU#zqh$C43q7BJWovo*>9E%ONw^{+7`;I+dDf zuu}9EzjCsSbOEx_ylaie6`>zZCH@`O7PnfRdf#kp(>jacf1Y&=AEVbJVRa~nUnVE` W=0jK>As2RxBv8~blwf9>C++o;z7 diff --git a/backend/main.py b/backend/main.py index a4add29..9be818c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -81,6 +81,58 @@ def add_app(entry: AppEntry): return {"status": "added"} +@router.post("/edit_app") +def edit_app(entry: AppEntry): + if not APPS_FILE.exists(): + return {"error": "apps.yaml not found"} + + with open(APPS_FILE, "r") as f: + current = yaml.safe_load(f) or {"sections": []} + + updated = False + for section in current["sections"]: + if section["name"] == entry.section: + for i, app in enumerate(section["apps"]): + if app["name"] == entry.app.name: + section["apps"][i] = entry.app.dict() + updated = True + break + break + + if not updated: + return {"error": "App not found to edit"}, 404 + + 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(): + return {"error": "apps.yaml not found"} + + with open(APPS_FILE, "r") as f: + current = yaml.safe_load(f) or {"sections": []} + + deleted = False + for section in current["sections"]: + if section["name"] == entry.section: + original_len = len(section["apps"]) + section["apps"] = [a for a in section["apps"] if a["name"] != entry.app.name] + if len(section["apps"]) != original_len: + deleted = True + break + + if not deleted: + return {"error": "App not found to delete"}, 404 + + with open(APPS_FILE, "w") as f: + yaml.safe_dump(current, f) + + return {"status": "deleted"} + + @router.get("/icon/{filename}") def get_public_icon_url(filename: str): url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}" diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..14a6151 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Custom files +env.js \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ceaafb0..4077ff1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,19 +1,49 @@ import { useEffect, useState } from 'react'; import './App.css'; import SectionGrid from './components/SectionGrid'; -import AddAppModal from './components/AddAppModal'; +import AppModal from './components/AppModal'; +import ConfirmDialog from './components/ConfirmDialog'; import Clock from './components/Clock'; -import { fetchSections } from './services/api'; // ✅ שימוש בפונקציה הנכונה +import { IoIosAdd } from 'react-icons/io'; +import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api'; function App() { const [sections, setSections] = useState([]); + const [editData, setEditData] = useState(null); + const [showAdd, setShowAdd] = useState(false); + const [confirmData, setConfirmData] = useState(null); - useEffect(() => { + const loadSections = () => { fetchSections() .then(data => setSections(data.sections || [])) .catch(err => console.error('Failed to fetch sections:', err)); + }; + + useEffect(() => { + loadSections(); }, []); + const handleDelete = (app) => { + setConfirmData({ + message: `Are you sure you want to delete "${app.name}"?`, + app + }); + }; + + const confirmDelete = async () => { + if (!confirmData?.app) return; + + try { + await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app }); + loadSections(); + } catch (err) { + console.error('Failed to delete app:', err); + alert('Failed to delete app'); + } finally { + setConfirmData(null); + } + }; + return (
@@ -21,12 +51,52 @@ function App() { Navix logo Navix - window.location.reload()} /> - {sections.map((section) => ( - + + {showAdd && ( + { + setShowAdd(false); + loadSections(); + }} + sections={sections} + /> + )} + + {editData && ( + { + setEditData(null); + loadSections(); + }} + sections={sections} + /> + )} + + {confirmData && ( + setConfirmData(null)} + /> + )} + + {sections.map(section => ( + ))} + + setShowAdd(true)} />
); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/AddAppModal.jsx b/frontend/src/components/AddAppModal.jsx deleted file mode 100644 index c37ee16..0000000 --- a/frontend/src/components/AddAppModal.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from 'react'; -import { addAppToSection, fetchSections } from '../services/api'; -import '../style/AddAppModal.css'; -import { IoIosAdd } from "react-icons/io"; - -function AddAppModal({ onAdded }) { - const [open, setOpen] = useState(false); - - const [section, setSection] = useState(''); - const [customSection, setCustomSection] = useState(false); - const [name, setName] = useState(''); - const [icon, setIcon] = useState(''); - const [description, setDescription] = useState(''); - const [url, setUrl] = useState(''); - const [sections, setSections] = useState([]); - - useEffect(() => { - fetchSections().then(data => setSections(data.sections || [])); - }, []); - - const handleSubmit = async (e) => { - e.preventDefault(); - try { - await addAppToSection({ section, app: { name, icon, description, url } }); - setOpen(false); - setSection(''); - setName(''); - setIcon(''); - setDescription(''); - setUrl(''); - setCustomSection(false); - if (onAdded) onAdded(); - } catch (err) { - alert('Failed to add app'); - } - }; - - return ( - <> - setOpen(true)} /> - - {open && ( -
setOpen(false)}> -
e.stopPropagation()}> -

Add New App

-
- - - {customSection && ( - setSection(e.target.value)} - required - /> - )} - - setName(e.target.value)} required /> - setIcon(e.target.value)} /> - setDescription(e.target.value)} /> - setUrl(e.target.value)} required /> - -
-
-
- )} - - ); -} - -export default AddAppModal; diff --git a/frontend/src/components/AppCard.jsx b/frontend/src/components/AppCard.jsx index 9fe7f08..e34f6bd 100644 --- a/frontend/src/components/AppCard.jsx +++ b/frontend/src/components/AppCard.jsx @@ -1,32 +1,38 @@ import { useEffect, useState } from 'react'; import '../style/AppCard.css'; -import { getIconUrl } from '../services/api'; +import { getIconUrl } from '../services/api'; +import { FaEdit, FaTrash } from 'react-icons/fa'; -function AppCard({ app }) { +function AppCard({ app, section, onEdit, onDelete }) { const [iconUrl, setIconUrl] = useState(null); useEffect(() => { - if (app.icon) { - getIconUrl(app.icon) - .then((url) => { - console.log('Presigned icon URL for', app.name, ':', url); - setIconUrl(url); - }) - .catch((err) => { - console.error(`Failed to load icon for ${app.name}:`, err); - }); - } -}, [app.icon, app.name]); + if (app.icon) { + getIconUrl(app.icon) + .then((url) => setIconUrl(url)) + .catch((err) => console.error(`Failed to load icon for ${app.name}:`, err)); + } + }, [app.icon, app.name]); return (
+
+ { + e.preventDefault(); + if (typeof onDelete === 'function') { + onDelete({ ...app, section }); + } + }} /> + { + e.preventDefault(); + if (typeof onEdit === 'function') { + onEdit({ ...app, section }); + } + }} /> +
- {iconUrl ? ( - {app.name} - ) : ( - ⚠️ - )} + {iconUrl ? {app.name} : ⚠️}

{app.name}

{app.description}

@@ -35,4 +41,4 @@ function AppCard({ app }) { ); } -export default AppCard; +export default AppCard; \ No newline at end of file diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index f527f0f..e394c07 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -1,14 +1,14 @@ import AppCard from './AppCard'; import '../style/AppGrid.css'; -function AppGrid({ apps }) { +function AppGrid({ apps, section, onEdit, onDelete }) { return (
{apps.map(app => ( - + ))}
); } -export default AppGrid; +export default AppGrid; \ No newline at end of file diff --git a/frontend/src/components/AppModal.jsx b/frontend/src/components/AppModal.jsx new file mode 100644 index 0000000..fcff3c7 --- /dev/null +++ b/frontend/src/components/AppModal.jsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import '../style/AddAppModal.css'; + +function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) { + const [open, setOpen] = useState(true); + const [section, setSection] = useState(initialData.section || ''); + const [customSection, setCustomSection] = useState(false); + const [name, setName] = useState(initialData.name || ''); + const [icon, setIcon] = useState(initialData.icon || ''); + const [description, setDescription] = useState(initialData.description || ''); + const [url, setUrl] = useState(initialData.url || ''); + + const handleSubmit = async (e) => { + e.preventDefault(); + await onSubmit({ section, app: { name, icon, description, url } }); + setOpen(false); + if (onClose) onClose(); + }; + + if (!open) return null; + + return ( +
{ setOpen(false); if (onClose) onClose(); }}> +
e.stopPropagation()}> +

{mode === 'edit' ? 'Edit App' : 'Add New App'}

+
+ + + {customSection && ( + setSection(e.target.value)} + required + /> + )} + + setName(e.target.value)} required /> + setIcon(e.target.value)} /> + setDescription(e.target.value)} /> + setUrl(e.target.value)} required /> + +
+
+
+ ); +} + +export default AppModal; \ No newline at end of file diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx new file mode 100644 index 0000000..8eb6119 --- /dev/null +++ b/frontend/src/components/ConfirmDialog.jsx @@ -0,0 +1,18 @@ +import '../style/AddAppModal.css'; // reuse the modal style + +function ConfirmDialog({ message, onConfirm, onCancel }) { + return ( +
+
e.stopPropagation()}> +

Confirm

+

{message}

+
+ + +
+
+
+ ); +} + +export default ConfirmDialog; diff --git a/frontend/src/components/SectionGrid.jsx b/frontend/src/components/SectionGrid.jsx index 2a79207..dfe326c 100644 --- a/frontend/src/components/SectionGrid.jsx +++ b/frontend/src/components/SectionGrid.jsx @@ -1,11 +1,11 @@ import AppGrid from './AppGrid'; import '../style/SectionGrid.css'; -function SectionGrid({ section }) { +function SectionGrid({ section, onEdit, onDelete }) { return (

{section.name}

- +
); } diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index c483f9c..6794a9e 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -22,3 +22,25 @@ export async function getIconUrl(filename) { const data = await res.json(); return data.url; } + +export async function editAppInSection({ section, app }) { + const res = await fetch(`${API_BASE}/edit_app`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ section, app }) + }); + + if (!res.ok) throw new Error(await res.text()); + return await res.json(); +} + +export async function deleteAppFromSection({ section, app }) { + const res = await fetch(`${API_BASE}/delete_app`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ section, app }) + }); + + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} diff --git a/frontend/src/style/AppCard.css b/frontend/src/style/AppCard.css index bcce67d..b859924 100644 --- a/frontend/src/style/AppCard.css +++ b/frontend/src/style/AppCard.css @@ -50,3 +50,31 @@ color: #ccc; } +.app-card-wrapper { + position: relative; +} + +.card-icons-inside { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 6px; + z-index: 2; +} + +.edit-icon, +.delete-icon { + background-color: rgba(0, 0, 0, 0.6); + border-radius: 50%; + padding: 4px; + color: white; + cursor: pointer; + font-size: 0.8rem; + transition: transform 0.2s ease; +} + +.edit-icon:hover, +.delete-icon:hover { + transform: scale(1.2); +} From ed037c5308a6c3b3d7d882db1e71e2841181caa7 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 4 Jul 2025 06:35:22 +0300 Subject: [PATCH 2/2] Update the pipeline --- .woodpecker.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 3618ddf..a7f2a5e 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -9,7 +9,7 @@ steps: include: [ frontend/** ] settings: registry: harbor.dvirlabs.com - repo: my-apps/navix-frontend + repo: my-apps/${CI_REPO_NAME}-frontend dockerfile: frontend/Dockerfile context: frontend tags: @@ -30,7 +30,7 @@ steps: include: [ backend/** ] settings: registry: harbor.dvirlabs.com - repo: my-apps/navix-backend + repo: my-apps/${CI_REPO_NAME}-backend dockerfile: backend/Dockerfile context: backend tags: @@ -63,8 +63,8 @@ steps: - | TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" echo "💡 Setting frontend tag to: $TAG" - yq -i ".frontend.tag = \"$TAG\"" manifests/navix/values.yaml - git add manifests/navix/values.yaml + yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml git commit -m "frontend: update tag to $TAG" || echo "No changes" git push origin HEAD @@ -89,7 +89,7 @@ steps: - | TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" echo "💡 Setting backend tag to: $TAG" - yq -i ".backend.tag = \"$TAG\"" manifests/navix/values.yaml - git add manifests/navix/values.yaml + yq -i ".backend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml + git add manifests/${CI_REPO_NAME}/values.yaml git commit -m "backend: update tag to $TAG" || echo "No changes" git push origin HEAD