Compare commits
No commits in common. "master" and "add-buttons" have entirely different histories.
master
...
add-button
@ -14,7 +14,7 @@ steps:
|
|||||||
context: frontend
|
context: frontend
|
||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
- ${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_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}
|
||||||
username:
|
username:
|
||||||
from_secret: DOCKER_USERNAME
|
from_secret: DOCKER_USERNAME
|
||||||
password:
|
password:
|
||||||
@ -85,10 +85,6 @@ 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}"
|
||||||
@ -97,22 +93,3 @@ 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
|
|
||||||
|
|||||||
Binary file not shown.
@ -35,13 +35,3 @@ 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
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
@ -21,6 +20,7 @@ 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,13 +34,12 @@ 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():
|
||||||
@ -49,19 +48,15 @@ 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):
|
||||||
@ -86,7 +81,6 @@ 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():
|
||||||
@ -99,21 +93,20 @@ 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.original_name or entry.app.name):
|
if app["name"] == 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 JSONResponse(status_code=404, content={"error": "App not found to edit"})
|
return {"error": "App not found to edit"}, 404
|
||||||
|
|
||||||
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():
|
||||||
@ -132,7 +125,7 @@ def delete_app(entry: AppEntry):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not deleted:
|
if not deleted:
|
||||||
return JSONResponse(status_code=404, content={"error": "App not found to delete"})
|
return {"error": "App not found to delete"}, 404
|
||||||
|
|
||||||
with open(APPS_FILE, "w") as f:
|
with open(APPS_FILE, "w") as f:
|
||||||
yaml.safe_dump(current, f)
|
yaml.safe_dump(current, f)
|
||||||
@ -145,10 +138,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)
|
||||||
|
|
||||||
|
|||||||
@ -27,3 +27,4 @@ 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;"]
|
||||||
|
|
||||||
|
|||||||
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
@ -11,8 +11,7 @@
|
|||||||
"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",
|
||||||
@ -1489,14 +1488,6 @@
|
|||||||
"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",
|
||||||
@ -2449,18 +2440,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -13,8 +13,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -16,14 +16,6 @@ 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;
|
||||||
@ -41,38 +33,3 @@ 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 וכו') */
|
|
||||||
|
|||||||
@ -4,30 +4,18 @@ 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 CustomToast from './components/CustomToast';
|
import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api';
|
||||||
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 => {
|
.then(data => setSections(data.sections || []))
|
||||||
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));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,7 +26,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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,83 +34,32 @@ function App() {
|
|||||||
if (!confirmData?.app) return;
|
if (!confirmData?.app) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteAppFromSection({
|
await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app });
|
||||||
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);
|
||||||
toast.error('Failed to delete app');
|
alert('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 />
|
||||||
|
<h1 className="main-title">
|
||||||
{/* 🔹 לוגו וכותרת במרכז */}
|
<img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" />
|
||||||
<div className="title-wrapper">
|
Navix
|
||||||
<h1 className="main-title">
|
</h1>
|
||||||
<img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" />
|
|
||||||
Navix
|
|
||||||
</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={handleAddSubmit}
|
onSubmit={addAppToSection}
|
||||||
onClose={() => setShowAdd(false)}
|
onClose={() => {
|
||||||
|
setShowAdd(false);
|
||||||
|
loadSections();
|
||||||
|
}}
|
||||||
sections={sections}
|
sections={sections}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -131,8 +68,11 @@ function App() {
|
|||||||
<AppModal
|
<AppModal
|
||||||
mode="edit"
|
mode="edit"
|
||||||
initialData={editData}
|
initialData={editData}
|
||||||
onSubmit={handleEditSubmit}
|
onSubmit={editAppInSection}
|
||||||
onClose={() => setEditData(null)}
|
onClose={() => {
|
||||||
|
setEditData(null);
|
||||||
|
loadSections();
|
||||||
|
}}
|
||||||
sections={sections}
|
sections={sections}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -145,40 +85,16 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredSections.map((section) => (
|
{sections.map(section => (
|
||||||
<SectionGrid
|
<SectionGrid
|
||||||
key={section.name}
|
key={section.name}
|
||||||
section={section}
|
section={section}
|
||||||
onEdit={(app) =>
|
onEdit={setEditData}
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, 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,24 +12,7 @@ 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();
|
||||||
};
|
};
|
||||||
@ -57,10 +40,8 @@ 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}>
|
<option key={s.name} value={s.name}>{s.name}</option>
|
||||||
{s.name}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
<option value="__new__">➕ Create new section...</option>
|
<option value="__new__">➕ Create new section...</option>
|
||||||
</select>
|
</select>
|
||||||
@ -70,37 +51,15 @@ 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
|
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required />
|
||||||
type="text"
|
<input type="text" placeholder="Icon URL" value={icon} onChange={e => setIcon(e.target.value)} />
|
||||||
placeholder="App name"
|
<input type="text" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} />
|
||||||
value={name}
|
<input type="text" placeholder="App URL" value={url} onChange={e => setUrl(e.target.value)} required />
|
||||||
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>
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,49 +1,11 @@
|
|||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,11 +23,11 @@ export async function getIconUrl(filename) {
|
|||||||
return data.url;
|
return data.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editAppInSection({ section, app, original_name }) {
|
export async function editAppInSection({ section, app }) {
|
||||||
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, original_name })
|
body: JSON.stringify({ section, app })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@ -1,46 +1,15 @@
|
|||||||
.section {
|
.section {
|
||||||
margin: 0 auto 3rem auto;
|
margin-bottom: 3rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 1000px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +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:
|
|
||||||
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 }}
|
|
||||||
11
navix-helm/templates/env-configmap.yaml
Normal file
11
navix-helm/templates/env-configmap.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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 }}"
|
||||||
|
};
|
||||||
44
navix-helm/templates/frontend-deployment.yaml
Normal file
44
navix-helm/templates/frontend-deployment.yaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
@ -1,8 +1,8 @@
|
|||||||
frontend:
|
frontend:
|
||||||
image:
|
image:
|
||||||
repository: harbor.dvirlabs.com/my-apps/navix-front
|
repository: harbor.dvirlabs.com/my-apps/navix-frontend
|
||||||
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-back
|
repository: harbor.dvirlabs.com/my-apps/navix-backend
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
tag: master-4fc5494
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 8000
|
port: 8000
|
||||||
@ -42,7 +42,8 @@ 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: navix.dvirlabs.com
|
- host: api-navix.dvirlabs.com
|
||||||
paths:
|
paths:
|
||||||
- path: /api
|
- path: /api
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user