Compare commits

...

33 Commits

Author SHA1 Message Date
f87719e03b Test pipeline 2025-08-05 03:39:07 +03:00
1a1b37266c Test pipeline 2025-08-05 03:32:01 +03:00
dvirlabs
ff97d4f2eb test 2025-07-28 16:08:02 +03:00
dvirlabs
e56328bd13 test 2025-07-23 13:38:40 +03:00
dvirlabs
cfb4281744 Test build 2025-07-22 18:32:59 +03:00
ffaa2f6c52 Import uvicorn at the begin 2025-07-10 21:21:29 +03:00
dvirlabs
35d70d06c8 Set new message for the pipeline 2025-07-07 19:01:18 +03:00
62a27698bf build front and back 2025-07-06 19:44:02 +03:00
bd9e108934 Update pipeline 2025-07-06 19:37:58 +03:00
ffa4f6606d Merge branch 'master' into revert-to-clean 2025-07-06 19:27:52 +03:00
e3e6e7339b Merge pull request 'prev version for real' (#25) from prev-version into master
Reviewed-on: #25
2025-07-06 16:03:58 +00:00
505b51e53a prev version for real 2025-07-06 19:03:06 +03:00
4b993c0251 Merge pull request 'Prev version' (#24) from fix-tag-pipeline into master
Reviewed-on: #24
2025-07-06 15:27:54 +00:00
5bbdce4f03 Prev version 2025-07-06 18:22:19 +03:00
83ef4f6435 Try tigger gitops pipeline 2025-07-06 00:47:24 +03:00
e64715eafc Try tigger gitops pipeline 2025-07-06 00:46:37 +03:00
1654d2b3d2 Trigger apps-gitops pipeline 2025-07-05 21:45:39 +03:00
8cf70319e6 Merge pull request 'develop' (#23) from develop into master
Reviewed-on: #23
2025-07-04 16:04:13 +00:00
52a1f2b4ae Update the pipeline to support tags 2025-07-04 19:03:40 +03:00
18c35aa398 Merge pull request 'try oidc' (#22) from oidc into develop
Reviewed-on: #22
2025-07-04 14:33:16 +00:00
cc1658c355 try oidc 2025-07-04 17:31:47 +03:00
8b68fa1b7a Merge pull request 'Set searchbox at the left side' (#21) from search-box into master
Reviewed-on: #21
2025-07-04 09:34:08 +00:00
d460d9ed72 Set searchbox at the left side 2025-07-04 12:32:57 +03:00
44ea62bce5 Merge pull request 'Add search box' (#20) from search-box into master
Reviewed-on: #20
2025-07-04 08:42:47 +00:00
62511cbc14 Add search box 2025-07-04 11:36:51 +03:00
3d503aa5b6 test the pipeline 2025-07-04 10:54:24 +03:00
2e27471f7f Merge pull request 'Add collapse button' (#19) from add-collapse-button into master
Reviewed-on: #19
2025-07-04 07:37:52 +00:00
f5b32c1b19 Add collapse button 2025-07-04 10:37:03 +03:00
985993bef2 Fix custom toast and add updated helmchart 2025-07-04 09:42:11 +03:00
c0c3c92b55 Add toast alerts and fix bugs 2025-07-04 07:26:24 +03:00
0945e7c7c2 Check the generic pipeline 2025-07-04 06:54:52 +03:00
f3475e200a Update icon url placeholder 2025-07-04 06:50:27 +03:00
2e0f554f3d Merge pull request 'add-buttons' (#18) from add-buttons into master
Reviewed-on: #18
2025-07-04 03:35:55 +00:00
24 changed files with 427 additions and 115 deletions

View File

@ -14,7 +14,7 @@ steps:
context: frontend context: frontend
tags: tags:
- latest - latest
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7} - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username: username:
from_secret: DOCKER_USERNAME from_secret: DOCKER_USERNAME
password: password:
@ -35,7 +35,7 @@ steps:
context: backend context: backend
tags: tags:
- latest - latest
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7} - ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username: username:
from_secret: DOCKER_USERNAME from_secret: DOCKER_USERNAME
password: password:
@ -85,6 +85,10 @@ steps:
- apk add --no-cache git yq - apk add --no-cache git yq
- git config --global user.name "woodpecker-bot" - git config --global user.name "woodpecker-bot"
- git config --global user.email "ci@dvirlabs.com" - git config --global user.email "ci@dvirlabs.com"
- |
if [ ! -d "my-apps" ]; then
git clone "https://${GIT_USERNAME}:${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
fi
- cd my-apps - cd my-apps
- | - |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}" TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
@ -93,3 +97,22 @@ steps:
git add 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 commit -m "backend: update tag to $TAG" || echo "No changes"
git push origin HEAD git push origin HEAD
trigger-gitops-via-push:
name: Trigger apps-gitops via Git push
image: alpine/git
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands: |
git config --global user.name "woodpecker-bot"
git config --global user.email "ci@dvirlabs.com"
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git"
cd apps-gitops
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
git add .trigger
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
git push origin HEAD

View File

@ -35,3 +35,13 @@ sections:
name: Vault name: Vault
url: https://vault.dvirlabs.com url: https://vault.dvirlabs.com
name: Dev-tools name: Dev-tools
- apps: []
name: fgbhn
- apps: []
name: dfgb
- apps:
- description: opensource s3 app
icon: minio.svg
name: Minio
url: https://minio.dvirlabs.com
name: Infra

View File

@ -1,6 +1,7 @@
from fastapi import FastAPI, APIRouter from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import uvicorn
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import yaml import yaml
@ -20,7 +21,6 @@ app.add_middleware(
load_dotenv() load_dotenv()
# ENV
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY") MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY")
@ -34,12 +34,13 @@ minio_client = Minio(
) )
BUCKET = MINIO_BUCKET or "navix-icons" BUCKET = MINIO_BUCKET or "navix-icons"
APPS_FILE = Path(__file__).parent / "apps.yaml"
@router.get("/") @router.get("/")
def root(): def root():
return {"message": "Welcome to the FastAPI application!"} return {"message": "Welcome to the FastAPI application!"}
APPS_FILE = Path(__file__).parent / "apps.yaml"
@router.get("/apps") @router.get("/apps")
def get_apps(): def get_apps():
@ -48,15 +49,19 @@ def get_apps():
with open(APPS_FILE, "r") as f: with open(APPS_FILE, "r") as f:
return yaml.safe_load(f) return yaml.safe_load(f)
class AppData(BaseModel): class AppData(BaseModel):
name: str name: str
icon: str icon: str
description: str description: str
url: str url: str
class AppEntry(BaseModel): class AppEntry(BaseModel):
section: str section: str
app: AppData app: AppData
original_name: str | None = None
@router.post("/add_app") @router.post("/add_app")
def add_app(entry: AppEntry): def add_app(entry: AppEntry):
@ -81,6 +86,7 @@ def add_app(entry: AppEntry):
return {"status": "added"} return {"status": "added"}
@router.post("/edit_app") @router.post("/edit_app")
def edit_app(entry: AppEntry): def edit_app(entry: AppEntry):
if not APPS_FILE.exists(): if not APPS_FILE.exists():
@ -93,20 +99,21 @@ def edit_app(entry: AppEntry):
for section in current["sections"]: for section in current["sections"]:
if section["name"] == entry.section: if section["name"] == entry.section:
for i, app in enumerate(section["apps"]): 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() section["apps"][i] = entry.app.dict()
updated = True updated = True
break break
break break
if not updated: 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: with open(APPS_FILE, "w") as f:
yaml.safe_dump(current, f) yaml.safe_dump(current, f)
return {"status": "updated"} return {"status": "updated"}
@router.post("/delete_app") @router.post("/delete_app")
def delete_app(entry: AppEntry): def delete_app(entry: AppEntry):
if not APPS_FILE.exists(): if not APPS_FILE.exists():
@ -125,7 +132,7 @@ def delete_app(entry: AppEntry):
break break
if not deleted: 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: with open(APPS_FILE, "w") as f:
yaml.safe_dump(current, f) yaml.safe_dump(current, f)
@ -138,10 +145,10 @@ def get_public_icon_url(filename: str):
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}" url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
return JSONResponse(content={"url": url}) return JSONResponse(content={"url": url})
app.include_router(router, prefix="/api") app.include_router(router, prefix="/api")
# ✅ This is the missing part:
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -27,4 +27,3 @@ RUN dos2unix /docker-entrypoint.d/10-generate-env.sh && chmod +x /docker-entrypo
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -11,7 +11,8 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-toastify": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
@ -1488,6 +1489,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2440,6 +2449,18 @@
"node": ">=0.10.0" "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": { "node_modules/redux": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",

View File

@ -13,7 +13,8 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0" "react-icons": "^5.5.0",
"react-toastify": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",

View File

@ -16,6 +16,14 @@ body {
text-align: center; text-align: center;
} }
/* 🔹 מרכז כותרת */
.title-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.main-title { .main-title {
display: flex; display: flex;
align-items: center; align-items: center;
@ -33,3 +41,38 @@ body {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
/* 🔹 תיבת חיפוש בצד שמאל */
.search-wrapper {
width: 100%;
display: flex;
justify-content: flex-start;
max-width: 1200px;
margin-bottom: 2rem;
margin-left: 2rem;
}
.search-input {
background: transparent;
margin-bottom: 10px;
border: none;
border-bottom: 2px solid #2e6dc0;
color: white;
font-family: 'Rajdhani', sans-serif;
font-size: 1.1rem;
padding: 0.4rem 0;
width: 240px;
transition: border-color 0.3s ease;
outline: none;
}
.search-input::placeholder {
color: #7a8aa5;
font-style: italic;
}
.search-input:focus {
border-bottom: 2px solid #4a90e2;
}
/* (שאר סגנונות שלך כמו add-button וכו') */

View File

@ -4,18 +4,30 @@ import SectionGrid from './components/SectionGrid';
import AppModal from './components/AppModal'; import AppModal from './components/AppModal';
import ConfirmDialog from './components/ConfirmDialog'; import ConfirmDialog from './components/ConfirmDialog';
import Clock from './components/Clock'; import Clock from './components/Clock';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { IoIosAdd } from 'react-icons/io'; import { IoIosAdd } from 'react-icons/io';
import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api'; import CustomToast from './components/CustomToast';
import {
fetchSections,
addAppToSection,
editAppInSection,
deleteAppFromSection,
} from './services/api';
function App() { function App() {
const [sections, setSections] = useState([]); const [sections, setSections] = useState([]);
const [editData, setEditData] = useState(null); const [editData, setEditData] = useState(null);
const [showAdd, setShowAdd] = useState(false); const [showAdd, setShowAdd] = useState(false);
const [confirmData, setConfirmData] = useState(null); const [confirmData, setConfirmData] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const loadSections = () => { const loadSections = () => {
fetchSections() 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)); .catch(err => console.error('Failed to fetch sections:', err));
}; };
@ -26,7 +38,7 @@ function App() {
const handleDelete = (app) => { const handleDelete = (app) => {
setConfirmData({ setConfirmData({
message: `Are you sure you want to delete "${app.name}"?`, message: `Are you sure you want to delete "${app.name}"?`,
app app,
}); });
}; };
@ -34,32 +46,83 @@ function App() {
if (!confirmData?.app) return; if (!confirmData?.app) return;
try { try {
await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app }); await deleteAppFromSection({
section: confirmData.app.section,
app: confirmData.app,
});
toast(<CustomToast type="delete" message={`App "${confirmData.app.name}" deleted successfully!`} />);
loadSections(); loadSections();
} catch (err) { } catch (err) {
console.error('Failed to delete app:', err); console.error('Failed to delete app:', err);
alert('Failed to delete app'); toast.error('Failed to delete app');
} finally { } finally {
setConfirmData(null); setConfirmData(null);
} }
}; };
const handleAddSubmit = async (data) => {
try {
await addAppToSection(data);
toast(<CustomToast type="success" message={`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(<CustomToast type="edit" message={`App "${data.app.name}" updated successfully!`} />);
setEditData(null);
loadSections();
} catch (err) {
console.error('Failed to update app:', err);
toast.error('Failed to update app');
}
};
const filteredSections = sections.map(section => ({
...section,
apps: section.apps.filter(app =>
app.name.toLowerCase().includes(searchTerm.toLowerCase())
),
})).filter(section => section.apps.length > 0);
return ( return (
<div className="App"> <div className="App">
<Clock /> <Clock />
{/* 🔹 לוגו וכותרת במרכז */}
<div className="title-wrapper">
<h1 className="main-title"> <h1 className="main-title">
<img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" /> <img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" />
Navix Navix
</h1> </h1>
</div>
{/* 🔍 שורת חיפוש בצד שמאל */}
<div className="search-wrapper">
<input
type="text"
placeholder="Search apps..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
{showAdd && ( {showAdd && (
<AppModal <AppModal
mode="add" mode="add"
onSubmit={addAppToSection} onSubmit={handleAddSubmit}
onClose={() => { onClose={() => setShowAdd(false)}
setShowAdd(false);
loadSections();
}}
sections={sections} sections={sections}
/> />
)} )}
@ -68,11 +131,8 @@ function App() {
<AppModal <AppModal
mode="edit" mode="edit"
initialData={editData} initialData={editData}
onSubmit={editAppInSection} onSubmit={handleEditSubmit}
onClose={() => { onClose={() => setEditData(null)}
setEditData(null);
loadSections();
}}
sections={sections} sections={sections}
/> />
)} )}
@ -85,16 +145,40 @@ function App() {
/> />
)} )}
{sections.map(section => ( {filteredSections.map((section) => (
<SectionGrid <SectionGrid
key={section.name} key={section.name}
section={section} section={section}
onEdit={setEditData} onEdit={(app) =>
setEditData({ ...app, section: section.name, original_name: app.name })
}
onDelete={handleDelete} onDelete={handleDelete}
/> />
))} ))}
<IoIosAdd className="add-button" onClick={() => setShowAdd(true)} /> <IoIosAdd className="add-button" onClick={() => setShowAdd(true)} />
<ToastContainer
position="bottom-right"
autoClose={3000}
hideProgressBar={false}
newestOnTop
closeOnClick
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
toastStyle={{
backgroundColor: 'black',
borderRadius: '12px',
minHeight: '110px',
padding: '20px',
color: '#fff',
display: 'flex',
alignItems: 'center',
}}
bodyClassName="toast-body"
/>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useState } from 'react';
import '../style/AddAppModal.css'; import '../style/AddAppModal.css';
function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) { function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) {
@ -12,7 +12,24 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); 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); setOpen(false);
if (onClose) onClose(); if (onClose) onClose();
}; };
@ -40,8 +57,10 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
disabled={mode === 'edit'} disabled={mode === 'edit'}
> >
<option value="" disabled>Select a section</option> <option value="" disabled>Select a section</option>
{sections.map(s => ( {sections.map((s) => (
<option key={s.name} value={s.name}>{s.name}</option> <option key={s.name} value={s.name}>
{s.name}
</option>
))} ))}
<option value="__new__"> Create new section...</option> <option value="__new__"> Create new section...</option>
</select> </select>
@ -51,15 +70,37 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections
type="text" type="text"
placeholder="New section name" placeholder="New section name"
value={section} value={section}
onChange={e => setSection(e.target.value)} onChange={(e) => setSection(e.target.value)}
required required
/> />
)} )}
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required /> <input
<input type="text" placeholder="Icon URL" value={icon} onChange={e => setIcon(e.target.value)} /> type="text"
<input type="text" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} /> placeholder="App name"
<input type="text" placeholder="App URL" value={url} onChange={e => setUrl(e.target.value)} required /> 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> <button type="submit">{mode === 'edit' ? 'Update' : 'Add'}</button>
</form> </form>
</div> </div>

View File

@ -0,0 +1,17 @@
import { FaCheckCircle, FaEdit, FaTrash } from 'react-icons/fa';
import '../style/CustomToast.css'; // Ensure you have this CSS file for styling
const iconMap = {
success: <FaCheckCircle color="#00ff00" />,
edit: <FaEdit color="#ffa500" />,
delete: <FaTrash color="#ff4d4d" />
};
export default function CustomToast({ type, message }) {
return (
<div className="custom-toast">
<span className="icon">{iconMap[type]}</span>
<span className="message">{message}</span>
</div>
);
}

View File

@ -1,11 +1,49 @@
import { useState, useRef, useEffect } from 'react';
import AppGrid from './AppGrid'; import AppGrid from './AppGrid';
import { FaChevronDown } from 'react-icons/fa';
import '../style/SectionGrid.css'; import '../style/SectionGrid.css';
function SectionGrid({ section, onEdit, onDelete }) { function SectionGrid({ section, onEdit, onDelete }) {
const [collapsed, setCollapsed] = useState(false);
const contentRef = useRef(null);
const [height, setHeight] = useState('auto');
const toggle = () => {
setCollapsed(prev => !prev);
};
useEffect(() => {
if (!contentRef.current) return;
if (!collapsed) {
const scrollHeight = contentRef.current.scrollHeight;
setHeight(`${scrollHeight}px`);
} else {
setHeight('0px');
}
}, [collapsed, section.apps?.length]);
return ( return (
<div className="section"> <div className="section">
<div className="section-header" onClick={toggle}>
<h2 className="section-title">{section.name}</h2> <h2 className="section-title">{section.name}</h2>
<AppGrid apps={section.apps} section={section.name} onEdit={onEdit} onDelete={onDelete} /> <FaChevronDown className={`toggle-icon ${collapsed ? 'collapsed' : ''}`} />
</div>
<div
ref={contentRef}
className="section-content"
style={{ maxHeight: height }}
>
<div className="app-grid-wrapper">
<AppGrid
apps={section.apps}
section={section.name}
onEdit={onEdit}
onDelete={onDelete}
/>
</div>
</div>
</div> </div>
); );
} }

View File

@ -23,11 +23,11 @@ export async function getIconUrl(filename) {
return data.url; 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`, { const res = await fetch(`${API_BASE}/edit_app`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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()); if (!res.ok) throw new Error(await res.text());

View File

@ -0,0 +1,26 @@
.custom-toast {
display: flex;
align-items: center;
gap: 14px;
font-family: 'Orbitron', sans-serif;
font-size: 16px;
color: #ffffff;
line-height: 1.4;
}
.custom-toast .icon {
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
color: inherit;
}
.custom-toast .message {
flex: 1;
font-size: 15px;
}
.Toastify__progress-bar {
background: #4e9aff !important;
}

View File

@ -1,15 +1,46 @@
.section { .section {
margin-bottom: 3rem; margin: 0 auto 3rem auto;
width: 100%; width: 100%;
max-width: 1000px; max-width: 100%;
border-bottom: 1px solid #2e6dc0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 0.5rem 1rem;
} }
.section-title { .section-title {
font-family: 'Rajdhani', sans-serif; font-family: 'Rajdhani', sans-serif;
font-size: 1.8rem; font-size: 1.8rem;
text-align: left;
border-bottom: 1px solid #2e6dc0;
font-weight: 500; font-weight: 500;
margin-bottom: 1rem;
color: white; color: white;
margin: 0;
}
.toggle-icon {
font-size: 1.4rem;
color: #aaa;
transition: transform 0.25s ease, color 0.2s ease;
}
.toggle-icon:hover {
color: #2e6dc0;
}
.toggle-icon.collapsed {
transform: rotate(-90deg);
}
.section-content {
overflow: hidden;
transition: max-height 0.4s ease;
}
.app-grid-wrapper {
margin-bottom: 2rem;
padding: 0 1.5rem;
} }

View File

@ -0,0 +1,27 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: navix-frontend
spec:
replicas: 1
selector:
matchLabels:
app: navix-frontend
template:
metadata:
labels:
app: navix-frontend
spec:
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.tag }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- containerPort: 80
env:
- name: API_BASE
value: {{ .Values.frontend.env.API_BASE | quote }}
- name: MINIO_ENDPOINT
value: {{ .Values.frontend.env.MINIO_ENDPOINT | quote }}
- name: MINIO_BUCKET
value: {{ .Values.frontend.env.MINIO_BUCKET | quote }}

View File

@ -1,8 +1,8 @@
frontend: frontend:
image: image:
repository: harbor.dvirlabs.com/my-apps/navix-frontend repository: harbor.dvirlabs.com/my-apps/navix-front
tag: latest
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
tag: master-4fc5494
service: service:
type: ClusterIP type: ClusterIP
port: 80 port: 80
@ -21,12 +21,12 @@ frontend:
API_BASE: "https://navix.dvirlabs.com/api" API_BASE: "https://navix.dvirlabs.com/api"
MINIO_ENDPOINT: "s3.dvirlabs.com" MINIO_ENDPOINT: "s3.dvirlabs.com"
MINIO_BUCKET: "navix-icons" MINIO_BUCKET: "navix-icons"
tag: master-4fc5494
backend: backend:
image: image:
repository: harbor.dvirlabs.com/my-apps/navix-backend repository: harbor.dvirlabs.com/my-apps/navix-back
tag: latest
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
tag: master-4fc5494
service: service:
type: ClusterIP type: ClusterIP
port: 8000 port: 8000
@ -42,8 +42,7 @@ backend:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
hosts: hosts:
- host: api-navix.dvirlabs.com - host: navix.dvirlabs.com
paths: paths:
- path: /api - path: /api
pathType: Prefix pathType: Prefix

View File

@ -1,11 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: navix-frontend-env
data:
env.js: |
window.ENV = {
API_BASE: "{{ .Values.frontend.env.API_BASE }}",
MINIO_ENDPOINT: "{{ .Values.frontend.env.MINIO_ENDPOINT }}",
MINIO_BUCKET: "{{ .Values.frontend.env.MINIO_BUCKET }}"
};

View File

@ -1,44 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: navix-frontend
spec:
replicas: 1
selector:
matchLabels:
app: navix-frontend
template:
metadata:
labels:
app: navix-frontend
spec:
initContainers:
- name: copy-env
image: busybox
command: ["sh", "-c", "cp /config/env.js /env/env.js"]
volumeMounts:
- name: env-config
mountPath: /config
- name: env-volume
mountPath: /env
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.tag }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- containerPort: 80
volumeMounts:
- name: env-volume
mountPath: /usr/share/nginx/html/env.js
subPath: env.js
volumes:
- name: env-volume
emptyDir: {}
- name: env-config
configMap:
name: navix-frontend-env
items:
- key: env.js
path: env.js