Compare commits

..

89 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
ed037c5308 Update the pipeline 2025-07-04 06:35:22 +03:00
be3f9aa03b Add edit and delete buttons 2025-07-04 06:26:12 +03:00
dvirlabs
9f37cf6b58 Update app to fetch correctly 2025-07-03 15:51:47 +03:00
dvirlabs
fa26c24d15 Fetch the data correct 2025-07-03 14:38:38 +03:00
dvirlabs
4fc5494525 Debug 2025-07-03 08:16:16 +03:00
dvirlabs
924a335682 Merge branch 'master' of https://git.dvirlabs.com/dvirlabs/navix 2025-07-03 08:12:34 +03:00
dvirlabs
283881b293 test 2025-07-03 08:12:29 +03:00
6e294363ff Debug 2025-07-03 07:59:59 +03:00
cb990e8af2 Debug 2025-07-03 07:46:24 +03:00
8c1d55b008 Debug 2025-07-03 07:44:34 +03:00
b8b0ab3dc4 Debug 2025-07-03 07:35:29 +03:00
b5122bc289 Debug 2025-07-03 07:15:40 +03:00
9dfb32b612 Debug 2025-07-03 07:13:48 +03:00
20af0dd38d Fix Docker 2025-07-03 07:11:29 +03:00
dvirlabs
2ba058fe10 Text build front and back 2025-07-02 09:08:05 +03:00
dvirlabs
aeb9f4a138 Text build front and back 2025-07-02 08:55:27 +03:00
6e55b088e9 Try fix dynamic env 2025-07-02 04:40:10 +03:00
a26902e414 Try the new plugin 2025-06-18 07:03:04 +03:00
af37eae616 Try the new plugin 2025-06-18 06:56:03 +03:00
068c7ed48a Try the new plugin 2025-06-18 06:48:26 +03:00
cec81e48c9 Try the new plugin 2025-06-18 06:21:45 +03:00
fdb03f983c Merge pull request 'micro-svc' (#17) from micro-svc into master
Reviewed-on: #17
2025-06-06 14:13:49 +00:00
727495f7c1 Add init container and cm for front 2025-06-06 17:11:32 +03:00
7cadc006d3 Fix connections between pods 2025-06-05 02:50:39 +03:00
e0dec56b1d Fix helm 2025-06-05 01:56:03 +03:00
543d02f7a2 Merge pull request 'Add helmchart' (#16) from helm into master
Reviewed-on: #16
2025-06-04 22:28:47 +00:00
dvirlabs
8a904d23c2 Add helmchart 2025-06-04 16:56:54 +03:00
dvirlabs
4c2e4844a4 test pipeline 2025-06-04 12:22:33 +03:00
dvirlabs
81daa6667f test pipeline 2025-06-04 12:21:28 +03:00
c1f09cc689 Merge pull request 'Fix the pipeline' (#15) from cicd into master
Reviewed-on: #15
2025-06-04 09:17:37 +00:00
dvirlabs
7790f06519 Fix the pipeline 2025-06-04 12:16:50 +03:00
a6b8d502e7 Merge pull request 'fix-add-app' (#14) from fix-add-app into master
Reviewed-on: #14
2025-06-04 01:14:12 +00:00
76ca036e2a Add drop down to sections 2025-06-04 04:13:17 +03:00
d6d7daf47c Merge pull request 'fix-cards-margin' (#13) from fix-cards-margin into style
Reviewed-on: #13
2025-06-04 01:01:05 +00:00
86b9facd77 fix cards margin 2025-06-04 04:00:11 +03:00
333da4cd80 Fake commit 2025-06-04 03:35:04 +03:00
050ad163fe Merge pull request 'style' (#12) from style into master
Reviewed-on: #12
2025-06-04 00:15:26 +00:00
b50de26e30 Merge pull request 'Fix the woodpecker yaml' (#11) from style-modal into style
Reviewed-on: #11
2025-06-04 00:15:03 +00:00
1f72fe4c57 Fix the woodpecker yaml 2025-06-04 03:14:22 +03:00
7f5de9e6b5 Merge pull request 'Fix the woodpecker yaml' (#10) from style-modal into style
Reviewed-on: #10
fix woodpecker.yaml
2025-06-04 00:13:10 +00:00
09fc4c45bd Fix the woodpecker yaml 2025-06-04 03:12:20 +03:00
464d510fc7 Merge pull request 'style' (#9) from style into master
Reviewed-on: #9
2025-06-03 23:59:05 +00:00
67067f5bd2 Merge pull request 'style-modal' (#8) from style-modal into style
Reviewed-on: #8
2025-06-03 23:58:36 +00:00
eefb99bc4c Style modal and add clock 2025-06-04 02:48:39 +03:00
5f5e6a951a Merge pull request 'style' (#7) from style into master
Reviewed-on: #7
2025-06-03 23:39:39 +00:00
1d673a6609 Merge pull request 'add-logo' (#6) from add-logo into style
Reviewed-on: #6
2025-06-03 23:38:58 +00:00
8eb5532b7b Style the modal 2025-06-04 02:36:42 +03:00
945b33485e Style the app with more neon and stuff 2025-06-04 02:31:30 +03:00
f23392b02e Merge pull request 'Change the docker image' (#4) from minio-coconnection into master
Reviewed-on: #4
2025-06-03 21:59:59 +00:00
dvirlabs
e5582b9b81 Change the docker image 2025-06-04 00:59:04 +03:00
804a95f1b1 Merge pull request 'minio-coconnection' (#3) from minio-coconnection into master
Reviewed-on: #3

fix pipeline
2025-06-03 21:56:47 +00:00
dvirlabs
4c29294d6a Change the pipeline 2025-06-04 00:54:57 +03:00
dvirlabs
36478eb507 Fix pull from minio 2025-06-04 00:36:06 +03:00
dvirlabs
fee9d2cce5 Add minio connection 2025-06-03 14:38:47 +03:00
129f1f9152 Merge pull request 'style-apps-card' (#2) from style-apps-card into master
Reviewed-on: #2
2025-06-03 05:10:28 +00:00
4d54f86a09 Merge pull request 'style-apps-card' (#1) from style-apps-card into master
Reviewed-on: #1
2025-06-03 04:53:50 +00:00
43 changed files with 1374 additions and 191 deletions

View File

@ -1,40 +1,118 @@
when:
event:
- push
- pull_request
branch:
- master
steps:
- name: tag
image: alpine
commands:
- export TAG_DATE=$(date +%Y%m%d)
- export SHORT_SHA=${CI_COMMIT_SHA:0:7}
- echo "TAGS=latest,$TAG_DATE-$SHORT_SHA" > .tags.env
- name: build-frontend
image: woodpeckerci/plugin-docker
build-frontend:
name: Build & Push Frontend
image: woodpeckerci/plugin-kaniko
when:
branch: [ master, develop ]
event: [ push, pull_request, tag ]
path:
include: [ frontend/** ]
settings:
repo: harbor.dvirlabs.com/my-apps/navix-frontend
tag_file: .tags.env
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-frontend
dockerfile: frontend/Dockerfile
context: frontend
registry: harbor.dvirlabs.com
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username:
from_secret: harbor_user
from_secret: DOCKER_USERNAME
password:
from_secret: harbor_password
from_secret: DOCKER_PASSWORD
- name: build-backend
image: woodpeckerci/plugin-docker
build-backend:
name: Build & Push Backend
image: woodpeckerci/plugin-kaniko
when:
branch: [ master, develop ]
event: [ push, pull_request, tag ]
path:
include: [ backend/** ]
settings:
repo: harbor.dvirlabs.com/my-apps/navix-backend
tag_file: .tags.env
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-backend
dockerfile: backend/Dockerfile
context: backend
registry: harbor.dvirlabs.com
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
username:
from_secret: harbor_user
from_secret: DOCKER_USERNAME
password:
from_secret: harbor_password
from_secret: DOCKER_PASSWORD
update-values-frontend:
name: Update frontend tag in values.yaml
image: alpine:3.19
when:
branch: [ master, develop ]
event: [ push ]
path:
include: [ frontend/** ]
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands:
- apk add --no-cache git yq
- 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/my-apps.git"
- cd my-apps
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
echo "💡 Setting frontend tag to: $TAG"
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
update-values-backend:
name: Update backend tag in values.yaml
image: alpine:3.19
when:
branch: [ master, develop ]
event: [ push ]
path:
include: [ backend/** ]
environment:
GIT_USERNAME:
from_secret: GIT_USERNAME
GIT_TOKEN:
from_secret: GIT_TOKEN
commands:
- apk add --no-cache git yq
- git config --global user.name "woodpecker-bot"
- 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
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
echo "💡 Setting backend tag to: $TAG"
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
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

4
backend/.env Normal file
View File

@ -0,0 +1,4 @@
MINIO_ACCESS_KEY=TDJvsBmbkpUXpCw5M7LA
MINIO_SECRET_KEY=n9scR7W0MZy6FF0bznV98fSgXpdebIQjqZvEr1Yu
MINIO_ENDPOINT=s3.dvirlabs.com
MINIO_BUCKET=navix-icons

View File

@ -7,3 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000

Binary file not shown.

View File

@ -1,27 +1,47 @@
sections:
- apps:
- description: Dashboards
icon: http://192.168.10.118:1111/icons/grafana.svg
icon: grafana.svg
name: Grafana
url: https://grafana.dvirlabs.com
- description: Monitoring
icon: http://192.168.10.118:1111/icons/prometheus.svg
icon: prometheus.svg
name: Prometheus
url: https://prometheus.dvirlabs.com
- description: Kibana logs server
icon: kibana.svg
name: Kibana
url: https://kibana.dvirlabs.com
name: Monitoring
- apps:
- description: Git server
icon: http://192.168.10.118:1111/icons/gitea.svg
icon: gitea.svg
name: Gitea
url: https://git.dvirlabs.com
- description: Container registry
icon: http://192.168.10.118:1111/icons/harbor.svg
icon: harbor.svg
name: Harbor
url: https://harbor.dvirlabs.com
- description: CI/CD
icon: http://192.168.10.118:1111/icons/woodpecker-ci.svg
icon: woodpecker-ci.svg
name: Woodpecker
url: https://woodpecker.dvirlabs.com
- description: CD tool
icon: argocd.svg
name: ArgoCD
url: https://argocd.dvirlabs.com
- description: Hashicorp vault
icon: vault.svg
name: Vault
url: https://vault.dvirlabs.com
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,45 +1,69 @@
from fastapi import FastAPI
from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import uvicorn
import os
from dotenv import load_dotenv
import yaml
from pathlib import Path
from pydantic import BaseModel
from minio import Minio
app = FastAPI()
# Allow CORS for all origins
router = APIRouter()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Welcome to the FastAPI application!"}
load_dotenv()
# Path to apps.yaml (relative to backend/)
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY")
MINIO_BUCKET = os.getenv("MINIO_BUCKET")
minio_client = Minio(
MINIO_ENDPOINT,
access_key=MINIO_ACCESS_KEY,
secret_key=MINIO_SECRET_KEY,
secure=True
)
BUCKET = MINIO_BUCKET or "navix-icons"
APPS_FILE = Path(__file__).parent / "apps.yaml"
@app.get("/apps")
@router.get("/")
def root():
return {"message": "Welcome to the FastAPI application!"}
@router.get("/apps")
def get_apps():
if not APPS_FILE.exists():
return {"error": "apps.yaml not found"}
with open(APPS_FILE, "r") as f:
return yaml.safe_load(f)
class AppData(BaseModel):
name: str
icon: str
description: str
url: str
class AppEntry(BaseModel):
section: str
app: AppData
original_name: str | None = None
@app.post("/add_app")
@router.post("/add_app")
def add_app(entry: AppEntry):
if not APPS_FILE.exists():
current = {"sections": []}
@ -47,7 +71,6 @@ def add_app(entry: AppEntry):
with open(APPS_FILE, "r") as f:
current = yaml.safe_load(f) or {"sections": []}
# Find or create section
for section in current["sections"]:
if section["name"] == entry.section:
section["apps"].append(entry.app.dict())
@ -64,6 +87,68 @@ 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.original_name or entry.app.name):
section["apps"][i] = entry.app.dict()
updated = True
break
break
if not updated:
return JSONResponse(status_code=404, content={"error": "App not found to edit"})
with open(APPS_FILE, "w") as f:
yaml.safe_dump(current, f)
return {"status": "updated"}
@router.post("/delete_app")
def delete_app(entry: AppEntry):
if not APPS_FILE.exists():
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 JSONResponse(status_code=404, content={"error": "App not found to delete"})
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}"
return JSONResponse(content={"url": url})
app.include_router(router, prefix="/api")
# ✅ This is the missing part:
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

65
backend/requirements.txt Normal file
View File

@ -0,0 +1,65 @@
annotated-types==0.7.0
anyio==4.4.0
argon2-cffi==25.1.0
argon2-cffi-bindings==21.2.0
boto3==1.34.150
botocore==1.34.150
certifi==2024.6.2
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
cryptography==43.0.0
Deprecated==1.2.14
dnspython==2.6.1
email_validator==2.2.0
fastapi==0.111.0
fastapi-cli==0.0.4
gitdb==4.0.11
GitPython==3.1.43
h11==0.14.0
httpcore==1.0.5
httptools==0.6.1
httpx==0.27.0
idna==3.7
Jinja2==3.1.4
jmespath==1.0.1
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
minio==7.2.15
numpy==2.2.1
orjson==3.10.5
pandas==2.2.3
prometheus_client==0.21.1
pycparser==2.22
pycryptodome==3.23.0
pydantic==2.8.0
pydantic_core==2.20.0
PyGithub==2.3.0
Pygments==2.18.0
PyJWT==2.8.0
pymongo==4.11.2
PyNaCl==1.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-multipart==0.0.9
pytz==2024.2
PyYAML==6.0.1
requests==2.32.3
rich==13.7.1
s3transfer==0.10.2
shellingham==1.5.4
six==1.16.0
smmap==5.0.1
sniffio==1.3.1
starlette==0.37.2
typer==0.12.3
typing_extensions==4.12.2
tzdata==2024.2
ujson==5.10.0
urllib3==2.2.2
uvicorn==0.30.1
watchfiles==0.22.0
websockets==12.0
wrapt==1.16.0

1
frontend/.env Normal file
View File

@ -0,0 +1 @@
VITE_API_URL=http://localhost:8000

3
frontend/.gitignore vendored
View File

@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Custom files
env.js

View File

@ -0,0 +1,4 @@
#!/bin/sh
# Generate env.js from the template
envsubst < /etc/env/env.js.template > /usr/share/nginx/html/env.js

29
frontend/Dockerfile Normal file
View File

@ -0,0 +1,29 @@
# Stage 1: Build the frontend
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install --legacy-peer-deps
RUN npm run build
# Stage 2: Serve with nginx
FROM nginx:alpine
# Install dos2unix
RUN apk add --no-cache dos2unix
# Copy built app
COPY --from=builder /app/dist /usr/share/nginx/html
# ✅ Copy env.js.template
COPY public/env.js.template /etc/env/env.js.template
# ✅ Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# ✅ Add env generator script to nginx entrypoint hook
COPY 10-generate-env.sh /docker-entrypoint.d/10-generate-env.sh
RUN dos2unix /docker-entrypoint.d/10-generate-env.sh && chmod +x /docker-entrypoint.d/10-generate-env.sh
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/navix-logo.svg" />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500&family=Rajdhani:wght@500&display=swap" rel="stylesheet">
<script src="/env.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<title>Navix</title>
</head>
<body>
<div id="root"></div>

11
frontend/nginx.conf Normal file
View File

@ -0,0 +1,11 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@ -11,7 +11,8 @@
"react": "^19.1.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0"
"react-icons": "^5.5.0",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
@ -1488,6 +1489,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2440,6 +2449,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",

View File

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

View File

@ -0,0 +1,6 @@
window.ENV = {
API_BASE: "${API_BASE}",
MINIO_ENDPOINT: "${MINIO_ENDPOINT}",
MINIO_BUCKET: "${MINIO_BUCKET}"
};

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="256px" version="1.1" width="256px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="glow"><feGaussianBlur in="SourceGraphic" stdDeviation="5" /></filter></defs><polygon fill="none" filter="url(#glow)" points="128,10 246,128 128,246 10,128" stroke="cyan" stroke-width="10" /></svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@ -16,7 +16,63 @@ body {
text-align: center;
}
.main-title {
margin-bottom: 2rem;
font-size: 2rem;
/* 🔹 מרכז כותרת */
.title-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.main-title {
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
color: white;
gap: 0.6rem;
font-family: 'Orbitron', sans-serif;
}
.navix-logo {
width: 40px;
height: 40px;
display: inline-block;
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

@ -1,29 +1,184 @@
// src/App.jsx
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 { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { IoIosAdd } from 'react-icons/io';
import CustomToast from './components/CustomToast';
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);
const [searchTerm, setSearchTerm] = useState('');
const fetchSections = () => {
fetch('/apps')
.then(res => res.json())
.then(data => setSections(data.sections || []));
const loadSections = () => {
fetchSections()
.then(data => {
const filtered = (data.sections || []).filter(section => (section.apps?.length ?? 0) > 0);
setSections(filtered);
})
.catch(err => console.error('Failed to fetch sections:', err));
};
useEffect(() => {
fetchSections();
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,
});
toast(<CustomToast type="delete" message={`App "${confirmData.app.name}" deleted successfully!`} />);
loadSections();
} catch (err) {
console.error('Failed to delete app:', err);
toast.error('Failed to delete app');
} finally {
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 (
<div className="App">
<h1 className="main-title">🔷 Navix</h1>
<AddAppModal onAdded={() => window.location.reload()} />
{sections.map((section) => (
<SectionGrid key={section.name} section={section} />
<Clock />
{/* 🔹 לוגו וכותרת במרכז */}
<div className="title-wrapper">
<h1 className="main-title">
<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 && (
<AppModal
mode="add"
onSubmit={handleAddSubmit}
onClose={() => setShowAdd(false)}
sections={sections}
/>
)}
{editData && (
<AppModal
mode="edit"
initialData={editData}
onSubmit={handleEditSubmit}
onClose={() => setEditData(null)}
sections={sections}
/>
)}
{confirmData && (
<ConfirmDialog
message={confirmData.message}
onConfirm={confirmDelete}
onCancel={() => setConfirmData(null)}
/>
)}
{filteredSections.map((section) => (
<SectionGrid
key={section.name}
section={section}
onEdit={(app) =>
setEditData({ ...app, section: section.name, original_name: app.name })
}
onDelete={handleDelete}
/>
))}
<IoIosAdd className="add-button" onClick={() => setShowAdd(true)} />
<ToastContainer
position="bottom-right"
autoClose={3000}
hideProgressBar={false}
newestOnTop
closeOnClick
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
toastStyle={{
backgroundColor: 'black',
borderRadius: '12px',
minHeight: '110px',
padding: '20px',
color: '#fff',
display: 'flex',
alignItems: 'center',
}}
bodyClassName="toast-body"
/>
</div>
);
}

View File

@ -1,58 +0,0 @@
import { useState } from 'react';
import { addAppToSection } from '../services/api';
import '../style/AddAppModal.css';
import { IoIosAddCircleOutline } from "react-icons/io";
function AddAppModal({ onAdded }) {
const [open, setOpen] = useState(false);
const [section, setSection] = useState('');
const [name, setName] = useState('');
const [icon, setIcon] = useState('');
const [description, setDescription] = useState('');
const [url, setUrl] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await addAppToSection({ section, app: { name, icon, description, url } });
setOpen(false);
setSection('');
setName('');
setIcon('');
setDescription('');
setUrl('');
if (onAdded) onAdded();
} catch (err) {
alert('Failed to add app');
}
};
return (
<>
<IoIosAddCircleOutline
className="add-button"
onClick={() => setOpen(true)}
/>
{open && (
<div className="modal-overlay" onClick={() => setOpen(false)}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>Add New App</h2>
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Section" value={section} onChange={e => setSection(e.target.value)} required />
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required />
<input type="text" placeholder="Icon 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">Add</button>
</form>
</div>
</div>
)}
</>
);
}
export default AddAppModal;

View File

@ -1,15 +1,44 @@
import { useEffect, useState } from 'react';
import '../style/AppCard.css';
import { getIconUrl } from '../services/api';
import { FaEdit, FaTrash } from 'react-icons/fa';
function AppCard({ app, section, onEdit, onDelete }) {
const [iconUrl, setIconUrl] = useState(null);
useEffect(() => {
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]);
function AppCard({ app }) {
return (
<a href={app.url} className="app-card" target="_blank" rel="noreferrer">
<div className="app-icon-wrapper">
<img src={app.icon} alt={app.name} className="app-icon" />
</div>
<h3>{app.name}</h3>
<p>{app.description}</p>
</a>
<div className="app-card-wrapper">
<a href={app.url} className="app-card" target="_blank" rel="noreferrer">
<div className="card-icons-inside">
<FaTrash className="delete-icon" onClick={(e) => {
e.preventDefault();
if (typeof onDelete === 'function') {
onDelete({ ...app, section });
}
}} />
<FaEdit className="edit-icon" onClick={(e) => {
e.preventDefault();
if (typeof onEdit === 'function') {
onEdit({ ...app, section });
}
}} />
</div>
<div className="app-icon-wrapper">
{iconUrl ? <img src={iconUrl} alt={app.name} className="app-icon" /> : <span className="icon-placeholder"></span>}
</div>
<h3>{app.name}</h3>
<p>{app.description}</p>
</a>
</div>
);
}
export default AppCard;
export default AppCard;

View File

@ -1,14 +1,14 @@
import AppCard from './AppCard';
import '../style/AppGrid.css';
function AppGrid({ apps }) {
function AppGrid({ apps, section, onEdit, onDelete }) {
return (
<div className="app-grid">
{apps.map(app => (
<AppCard key={app.name} app={app} />
<AppCard key={app.name} app={app} section={section} onEdit={onEdit} onDelete={onDelete} />
))}
</div>
);
}
export default AppGrid;
export default AppGrid;

View File

@ -0,0 +1,111 @@
import { 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();
const appData = {
name,
icon,
description,
url,
};
const payload = {
section,
app: appData,
};
if (mode === 'edit') {
payload.original_name = initialData.original_name || initialData.name;
}
await onSubmit(payload);
setOpen(false);
if (onClose) onClose();
};
if (!open) return null;
return (
<div className="modal-overlay" onClick={() => { setOpen(false); if (onClose) onClose(); }}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>{mode === 'edit' ? 'Edit App' : 'Add New App'}</h2>
<form onSubmit={handleSubmit}>
<select
value={customSection ? '__new__' : section}
onChange={(e) => {
const val = e.target.value;
if (val === '__new__') {
setSection('');
setCustomSection(true);
} else {
setSection(val);
setCustomSection(false);
}
}}
required={!customSection}
disabled={mode === 'edit'}
>
<option value="" disabled>Select a section</option>
{sections.map((s) => (
<option key={s.name} value={s.name}>
{s.name}
</option>
))}
<option value="__new__"> Create new section...</option>
</select>
{customSection && (
<input
type="text"
placeholder="New section name"
value={section}
onChange={(e) => setSection(e.target.value)}
required
/>
)}
<input
type="text"
placeholder="App name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
type="text"
placeholder="Icon name (e.g. grafana.svg) or full URL"
value={icon}
onChange={(e) => setIcon(e.target.value)}
/>
<input
type="text"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<input
type="text"
placeholder="App URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
required
/>
<button type="submit">{mode === 'edit' ? 'Update' : 'Add'}</button>
</form>
</div>
</div>
);
}
export default AppModal;

View File

@ -0,0 +1,28 @@
import { useState, useEffect } from 'react';
import '../style/Clock.css';
function Clock() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const time = now.toLocaleTimeString('en-GB'); // HH:MM:SS
const date = now.toLocaleDateString('en-GB', {
weekday: 'short',
day: 'numeric',
month: 'short',
year: 'numeric'
});
return (
<div className="clock-container">
<span className="clock-time">{time}</span>
<span className="clock-date">{date}</span>
</div>
);
}
export default Clock;

View File

@ -0,0 +1,18 @@
import '../style/AddAppModal.css'; // reuse the modal style
function ConfirmDialog({ message, onConfirm, onCancel }) {
return (
<div className="modal-overlay" onClick={onCancel}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>Confirm</h2>
<p style={{ color: '#ccc', textAlign: 'center', marginBottom: '1.5rem' }}>{message}</p>
<div style={{ display: 'flex', gap: '1rem' }}>
<button style={{ background: 'gray' }} onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Delete</button>
</div>
</div>
</div>
);
}
export default ConfirmDialog;

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 { FaChevronDown } from 'react-icons/fa';
import '../style/SectionGrid.css';
function SectionGrid({ section }) {
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 (
<div className="section">
<h2 className="section-title">{section.name}</h2>
<AppGrid apps={section.apps} />
<div className="section-header" onClick={toggle}>
<h2 className="section-title">{section.name}</h2>
<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>
);
}

View File

@ -1,15 +1,46 @@
const API_BASE = window?.ENV?.API_BASE || "";
export async function fetchSections() {
const res = await fetch('/apps');
const res = await fetch(`${API_BASE}/apps`);
if (!res.ok) throw new Error('Failed to fetch sections');
return res.json();
}
export async function addAppToSection({ section, app }) {
const res = await fetch('/add_app', {
const res = await fetch(`${API_BASE}/add_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();
}
}
export async function getIconUrl(filename) {
const res = await fetch(`${API_BASE}/icon/${filename}`);
if (!res.ok) throw new Error(`Failed to fetch icon for ${filename}`);
const data = await res.json();
return data.url;
}
export async function editAppInSection({ section, app, original_name }) {
const res = await fetch(`${API_BASE}/edit_app`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ section, app, original_name })
});
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();
}

View File

@ -2,23 +2,25 @@
position: fixed;
bottom: 2rem;
right: 2rem;
background-color: #007bff;
color: white;
font-size: 2rem;
width: 64px;
height: 64px;
border-radius: 50%;
width: 60px;
height: 60px;
background: linear-gradient(145deg, #206064, #182b41);
box-shadow: 0 0 12px #00f0ff;
color: white;
font-size: 2.2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
transition: background-color 0.3s ease;
z-index: 1000;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: none;
}
.add-button:hover {
background-color: #0056b3;
transform: rotate(90deg) scale(1.1);
box-shadow: 0 0 20px #00f0ff;
}
.modal-overlay {
@ -34,44 +36,96 @@
z-index: 1000;
}
.modal {
background: #1e1e1e;
padding: 2rem;
border-radius: 12px;
width: 340px;
height: 420px;
box-shadow: 0 0 10px #000;
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Flexbox centering */
.modal {
background: #111;
padding: 2rem;
border-radius: 16px;
width: 380px;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Rajdhani', sans-serif;
animation: fadeInUp 0.3s ease-out;
}
.modal select {
width: 95%;
height: 40px;
padding: 0.6rem;
margin-bottom: 1rem;
background-color: #1f1f1f;
border: 1px solid #2c2c2c;
color: #fff;
border-radius: 8px;
transition: border 0.3s;
font-family: inherit;
font-size: 1rem;
}
.modal select:focus {
border: 1px solid #00f0ff;
outline: none;
box-shadow: 0 0 6px #00f0ff40;
}
.modal option {
background-color: #1f1f1f;
color: #fff;
}
.modal h2 {
color: #fff;
font-size: 1.8rem;
margin-bottom: 1.5rem;
text-shadow: 0 0 5px #00f0ff;
}
.modal input {
width: 90%;
height: 35px;
padding: 0.5rem;
margin-bottom: 0.75rem;
background-color: #2c2c2c;
border: none;
color: white;
border-radius: 6px;
width: 95%;
height: 40px;
padding: 0.6rem;
margin-bottom: 1rem;
background-color: #1f1f1f;
border: 1px solid #2c2c2c;
color: #fff;
border-radius: 8px;
transition: border 0.3s;
font-family: inherit;
}
.modal input:focus {
border: 1px solid #00f0ff;
outline: none;
box-shadow: 0 0 6px #00f0ff40;
}
.modal button {
width: 90%;
padding: 0.5rem;
background-color: #007bff;
width: 80%;
padding: 0.6rem;
background: linear-gradient(to right, #007bff, #00f0ff);
color: white;
font-weight: 600;
border: none;
border-radius: 6px;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s ease;
font-family: inherit;
}
.modal button:hover {
background-color: #0056b3;
background: linear-gradient(to right, #00f0ff, #007bff);
}
.add-button {

View File

@ -1,12 +1,12 @@
.app-card {
background-color: #1e1e1e;
color: #fff;
padding: 1.3rem;
padding: 0.9rem;
border-radius: 12px;
text-align: center;
text-decoration: none;
width: 150px;
height: 150px;
width: 170px;
height: 170px;
transition: transform 0.2s ease;
display: flex;
flex-direction: column;
@ -36,3 +36,45 @@
object-fit: contain;
display: block;
}
.app-card h3 {
font-family: 'Rajdhani', sans-serif;
font-size: 1.1rem;
margin: 0.4rem 0;
color: white;
}
.app-card p {
font-family: 'Rajdhani', sans-serif;
font-size: 0.9rem;
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);
}

View File

@ -0,0 +1,23 @@
.clock-container {
position: absolute;
top: 1.5rem;
right: 2rem;
text-align: right;
font-family: 'Rajdhani', sans-serif;
color: #00f0ff;
text-shadow: 0 0 6px rgba(0, 255, 255, 0.3);
font-size: 1rem;
line-height: 1.2;
user-select: none;
}
.clock-time {
font-size: 2.1rem;
display: block;
margin-bottom: 0.2rem;
}
.clock-date {
font-size: 1rem;
color: #999;
}

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,14 +1,46 @@
.section {
margin-bottom: 3rem;
margin: 0 auto 3rem auto;
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 {
font-size: 1.5rem;
color: #ffffff;
margin-bottom: 1rem;
border-bottom: 1px solid #444;
padding-bottom: 0.5rem;
text-align: left;
font-family: 'Rajdhani', sans-serif;
font-size: 1.8rem;
font-weight: 500;
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,14 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/apps': 'http://localhost:8000',
'/add_app': 'http://localhost:8000',
'/icon': 'http://localhost:8000',
}
}
});

View File

@ -4,10 +4,56 @@ import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/apps': 'http://localhost:8000',
'/add_app': 'http://localhost:8000',
}
}
});
// export default defineConfig({
// plugins: [react()],
// server: {
// proxy: {
// '/apps': 'http://localhost:8000',
// '/add_app': 'http://localhost:8000',
// '/icon': 'http://localhost:8000',
// }
// }
// });
// import { defineConfig } from 'vite';
// import react from '@vitejs/plugin-react';
// export default defineConfig({
// plugins: [react()],
// });

4
navix-chart/Chart.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v2
name: navix
version: 0.1.0
description: A DevOps dashboard called Navix

View File

@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: navix-backend
spec:
replicas: 1
selector:
matchLabels:
app: navix-backend
template:
metadata:
labels:
app: navix-backend
spec:
containers:
- name: backend
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.tag }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- containerPort: 8000
env:
- name: MINIO_ACCESS_KEY
value: "{{ .Values.backend.env.MINIO_ACCESS_KEY }}"
- name: MINIO_SECRET_KEY
value: "{{ .Values.backend.env.MINIO_SECRET_KEY }}"
- name: MINIO_ENDPOINT
value: "{{ .Values.backend.env.MINIO_ENDPOINT }}"
- name: MINIO_BUCKET
value: "{{ .Values.backend.env.MINIO_BUCKET }}"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: navix-backend
spec:
type: {{ .Values.backend.service.type }}
selector:
app: navix-backend
ports:
- port: {{ .Values.backend.service.port }}
targetPort: 8000

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

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: navix-frontend
spec:
type: {{ .Values.frontend.service.type }}
selector:
app: navix-frontend
ports:
- port: {{ .Values.frontend.service.port }}
targetPort: 80

View File

@ -0,0 +1,57 @@
{{- if .Values.frontend.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: navix-frontend
annotations:
{{- range $key, $value := .Values.frontend.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
spec:
ingressClassName: {{ .Values.frontend.ingress.className }}
rules:
{{- range .Values.frontend.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: navix-frontend
port:
number: {{ $.Values.frontend.service.port }}
{{- end }}
{{- end }}
{{- end }}
---
{{- if .Values.backend.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: navix-backend
annotations:
{{- range $key, $value := .Values.backend.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
spec:
ingressClassName: {{ .Values.backend.ingress.className }}
rules:
{{- range .Values.backend.ingress.hosts }}
- host: {{ .host }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: navix-backend
port:
number: {{ $.Values.backend.service.port }}
{{- end }}
{{- end }}
{{- end }}

48
navix-chart/values.yaml Normal file
View File

@ -0,0 +1,48 @@
frontend:
image:
repository: harbor.dvirlabs.com/my-apps/navix-front
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
hosts:
- host: navix.dvirlabs.com
paths:
- path: /
pathType: Prefix
env:
API_BASE: "https://navix.dvirlabs.com/api"
MINIO_ENDPOINT: "s3.dvirlabs.com"
MINIO_BUCKET: "navix-icons"
backend:
image:
repository: harbor.dvirlabs.com/my-apps/navix-back
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8000
env:
MINIO_ACCESS_KEY: "your-access-key"
MINIO_SECRET_KEY: "your-secret-key"
MINIO_ENDPOINT: "s3.dvirlabs.com"
MINIO_BUCKET: "navix-icons"
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
hosts:
- host: navix.dvirlabs.com
paths:
- path: /api
pathType: Prefix