Compare commits

..

No commits in common. "master" and "add-buttons" have entirely different histories.

24 changed files with 115 additions and 427 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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;"]

View File

@ -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",

View File

@ -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",

View File

@ -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 וכו') */

View File

@ -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 />
{/* 🔹 לוגו וכותרת במרכז */}
<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={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>
); );
} }

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
<FaChevronDown className={`toggle-icon ${collapsed ? 'collapsed' : ''}`} /> <AppGrid apps={section.apps} section={section.name} onEdit={onEdit} onDelete={onDelete} />
</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, 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());

View File

@ -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;
}

View File

@ -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;
} }

View File

@ -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 }}

View 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 }}"
};

View 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

View File

@ -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