Compare commits
80 Commits
eccdb2f072
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019208ad0e | ||
|
|
af4fd324eb | ||
|
|
3d30c84497 | ||
|
|
6f8247e4fd | ||
|
|
014dcb5b09 | ||
|
|
2f786aa14f | ||
|
|
2e9d2ca143 | ||
|
|
4f0e7013e4 | ||
|
|
c1df449f57 | ||
|
|
a07c3ccd9c | ||
| 5fe92cde25 | |||
|
|
a4e4cb4c5f | ||
|
|
ec5d9916ba | ||
| 34ce3ef998 | |||
| d70cbdb12f | |||
| b63597345a | |||
| a910b8270a | |||
| 27afda2d70 | |||
|
|
247d207e3b | ||
|
|
a12df3306d | ||
|
|
29eb7358df | ||
|
|
28daa5c078 | ||
|
|
79f17c423b | ||
|
|
50bcd35e68 | ||
|
|
61cd5c471a | ||
|
|
e83ecf4717 | ||
|
|
53b37c2e16 | ||
|
|
a9bf480954 | ||
|
|
4afbc53420 | ||
|
|
d0594fb161 | ||
|
|
7a0e0c95aa | ||
| 943cd6935b | |||
| 4a270dbfe3 | |||
|
|
e08ab62a32 | ||
|
|
c7ae399615 | ||
| 33d06bc94d | |||
| e2e5c683e4 | |||
|
|
838e145598 | ||
| 15e0f886d7 | |||
|
|
7c50f0a59a | ||
|
|
6508363c12 | ||
| 80242b9602 | |||
| 4641cbb3fa | |||
| 59001222a0 | |||
|
|
5e958ac8c3 | ||
| 7126c618ea | |||
|
|
543617cc08 | ||
|
|
a1151d5e89 | ||
|
|
10b95ec5ef | ||
|
|
bf5e5c7542 | ||
|
|
090361cf66 | ||
|
|
18e70cd445 | ||
| a256e01444 | |||
|
|
5920bf1617 | ||
|
|
122e3f2edc | ||
|
|
2584e92af2 | ||
|
|
1016fee300 | ||
|
|
cda0e99ce7 | ||
|
|
a08b7af8ca | ||
|
|
7305cef18c | ||
|
|
450faad45c | ||
|
|
16d1afc44b | ||
|
|
a607a3f15b | ||
|
|
009c3261c8 | ||
|
|
e7532f0324 | ||
| 7ea16fe48e | |||
| c4d325ecd3 | |||
| 9e3af1fc07 | |||
| bd6c2ce9c1 | |||
| 59defcefe5 | |||
| d9c6f490f0 | |||
| a3a2cec9cf | |||
| 2054181228 | |||
| 6c01329f27 | |||
| c177be326c | |||
| 972bb50c64 | |||
| 20a4ea1655 | |||
| 0856ca5b7a | |||
| 0e5abbbece | |||
| d69c209101 |
@@ -27,3 +27,9 @@ MINIO_SECURE=false
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
MAX_UPLOAD_SIZE=524288000
|
||||
|
||||
# NPM Configuration (for frontend build)
|
||||
# Default: https://registry.npmjs.org/ (public npm registry)
|
||||
# For restricted environments, set to your custom npm proxy/registry URL
|
||||
# Example: http://your-nexus-server:8081/repository/npm-proxy/
|
||||
NPM_REGISTRY=https://registry.npmjs.org/
|
||||
|
||||
26
.gitea/workflows/docker.yml
Normal file
26
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.bitstorm.ca # e.g., docker.io for Docker Hub
|
||||
username: ${{ secrets.REGISTRY_LOGIN }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: git.bitstorm.ca/bitforge/warehouse13:latest
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -86,3 +86,10 @@ helm/charts/
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# Node.js
|
||||
package-lock.json
|
||||
**/package-lock.json
|
||||
|
||||
# Built static files (generated during Docker build from Angular)
|
||||
static/
|
||||
|
||||
182
.gitlab-ci.yml
182
.gitlab-ci.yml
@@ -1,164 +1,42 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
# Test stage
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y gcc postgresql-client
|
||||
- pip install -r requirements.txt
|
||||
- pip install pytest pytest-asyncio httpx
|
||||
script:
|
||||
- echo "Running tests..."
|
||||
- python -m pytest tests/ -v || echo "No tests found, skipping"
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Lint stage
|
||||
lint:
|
||||
stage: test
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install flake8 black
|
||||
script:
|
||||
- echo "Running linters..."
|
||||
- flake8 app/ --max-line-length=120 --ignore=E203,W503 || true
|
||||
- black --check app/ || true
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
allow_failure: true
|
||||
|
||||
# Build Docker image
|
||||
build:
|
||||
build_container:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
|
||||
variables:
|
||||
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
|
||||
before_script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- mkdir -p /tmp/buildah-storage
|
||||
- export BUILDAH_ROOT="/tmp/buildah-storage"
|
||||
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
script:
|
||||
- echo "Building Docker image..."
|
||||
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
|
||||
- docker push $IMAGE_TAG
|
||||
- docker push $LATEST_TAG
|
||||
only:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
# - buildah bud --build-arg NPM_REGISTRY=https://deps.global.bsf.tools/artifactory/api/npm/registry.npmjs.org/ --storage-driver vfs --isolation chroot -t $IMAGE_NAME .
|
||||
- buildah pull git.bitstorm.ca/bitforge/warehouse13:latest
|
||||
- buildah tag git.bitstorm.ca/bitforge/warehouse13:latest $IMAGE_NAME
|
||||
- echo "Pushing $IMAGE_NAME"
|
||||
- buildah push $IMAGE_NAME
|
||||
|
||||
# Deploy to development
|
||||
deploy:dev:
|
||||
deploy_helm_charts:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
|
||||
image:
|
||||
name: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
|
||||
parallel:
|
||||
matrix:
|
||||
# - ENV: "prod"
|
||||
# VALUES_FILE: "helm/values-prod.yaml"
|
||||
# CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
|
||||
# NAMESPACE: "bsf-services-namespace"
|
||||
# ONLY: "main"
|
||||
- ENV: "dev"
|
||||
VALUES_FILE: "helm/warehouse13/values.yaml"
|
||||
CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
|
||||
NAMESPACE: "bsf-services-dev-namespace"
|
||||
# ONLY: ["branches", "!main"]
|
||||
script:
|
||||
- echo "Deploying to development environment..."
|
||||
- kubectl config use-context $CONTEXT
|
||||
- echo "Deploy - $CI_REGISTRY_NAME - $CI_COMMIT_REF_SLUG - $CI_COMMIT_SHORT_SHA"
|
||||
- |
|
||||
helm upgrade --install datalake-dev ./helm \
|
||||
--namespace datalake-dev \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake-dev.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
environment:
|
||||
name: development
|
||||
url: https://datalake-dev.example.com
|
||||
only:
|
||||
- develop
|
||||
when: manual
|
||||
|
||||
# Deploy to staging
|
||||
deploy:staging:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_STAGING" | base64 -d > ~/.kube/config
|
||||
script:
|
||||
- echo "Deploying to staging environment..."
|
||||
- |
|
||||
helm upgrade --install datalake-staging ./helm \
|
||||
--namespace datalake-staging \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake-staging.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--set resources.requests.cpu=1000m \
|
||||
--set resources.requests.memory=1Gi \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
environment:
|
||||
name: staging
|
||||
url: https://datalake-staging.example.com
|
||||
only:
|
||||
- main
|
||||
- master
|
||||
when: manual
|
||||
|
||||
# Deploy to production
|
||||
deploy:prod:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_PROD" | base64 -d > ~/.kube/config
|
||||
script:
|
||||
- echo "Deploying to production environment..."
|
||||
- |
|
||||
helm upgrade --install datalake ./helm \
|
||||
--namespace datalake-prod \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set replicaCount=3 \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--set resources.requests.cpu=2000m \
|
||||
--set resources.requests.memory=2Gi \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=3 \
|
||||
--set autoscaling.maxReplicas=10 \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
environment:
|
||||
name: production
|
||||
url: https://datalake.example.com
|
||||
only:
|
||||
- tags
|
||||
when: manual
|
||||
helm upgrade --install warehouse13-dev ./helm/warehouse13 --namespace $NAMESPACE -f $VALUES_FILE --set app.image.repository=$CI_REGISTRY_IMAGE --set app.image.tag=$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
|
||||
164
.gitlab-ci.yml.old
Normal file
164
.gitlab-ci.yml.old
Normal file
@@ -0,0 +1,164 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
# Test stage
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y gcc postgresql-client
|
||||
- pip install -r requirements.txt
|
||||
- pip install pytest pytest-asyncio httpx
|
||||
script:
|
||||
- echo "Running tests..."
|
||||
- python -m pytest tests/ -v || echo "No tests found, skipping"
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
|
||||
# Lint stage
|
||||
lint:
|
||||
stage: test
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install flake8 black
|
||||
script:
|
||||
- echo "Running linters..."
|
||||
- flake8 app/ --max-line-length=120 --ignore=E203,W503 || true
|
||||
- black --check app/ || true
|
||||
only:
|
||||
- branches
|
||||
- merge_requests
|
||||
allow_failure: true
|
||||
|
||||
# Build Docker image
|
||||
build:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
before_script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
script:
|
||||
- echo "Building Docker image..."
|
||||
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
|
||||
- docker push $IMAGE_TAG
|
||||
- docker push $LATEST_TAG
|
||||
only:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
|
||||
# Deploy to development
|
||||
deploy:dev:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
|
||||
script:
|
||||
- echo "Deploying to development environment..."
|
||||
- |
|
||||
helm upgrade --install datalake-dev ./helm \
|
||||
--namespace datalake-dev \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake-dev.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
environment:
|
||||
name: development
|
||||
url: https://datalake-dev.example.com
|
||||
only:
|
||||
- develop
|
||||
when: manual
|
||||
|
||||
# Deploy to staging
|
||||
deploy:staging:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_STAGING" | base64 -d > ~/.kube/config
|
||||
script:
|
||||
- echo "Deploying to staging environment..."
|
||||
- |
|
||||
helm upgrade --install datalake-staging ./helm \
|
||||
--namespace datalake-staging \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake-staging.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--set resources.requests.cpu=1000m \
|
||||
--set resources.requests.memory=1Gi \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
environment:
|
||||
name: staging
|
||||
url: https://datalake-staging.example.com
|
||||
only:
|
||||
- main
|
||||
- master
|
||||
when: manual
|
||||
|
||||
# Deploy to production
|
||||
deploy:prod:
|
||||
stage: deploy
|
||||
image: alpine/helm:latest
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
- chmod +x kubectl
|
||||
- mv kubectl /usr/local/bin/
|
||||
- mkdir -p ~/.kube
|
||||
- echo "$KUBE_CONFIG_PROD" | base64 -d > ~/.kube/config
|
||||
script:
|
||||
- echo "Deploying to production environment..."
|
||||
- |
|
||||
helm upgrade --install datalake ./helm \
|
||||
--namespace datalake-prod \
|
||||
--create-namespace \
|
||||
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||
--set replicaCount=3 \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake.example.com \
|
||||
--set ingress.hosts[0].paths[0].path=/ \
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||
--set resources.requests.cpu=2000m \
|
||||
--set resources.requests.memory=2Gi \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=3 \
|
||||
--set autoscaling.maxReplicas=10 \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
environment:
|
||||
name: production
|
||||
url: https://datalake.example.com
|
||||
only:
|
||||
- tags
|
||||
when: manual
|
||||
465
DEPLOYMENT.md
465
DEPLOYMENT.md
@@ -1,465 +0,0 @@
|
||||
# Deployment Guide
|
||||
|
||||
This guide covers deploying the Test Artifact Data Lake in various environments.
|
||||
|
||||
## Table of Contents
|
||||
- [Local Development](#local-development)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Kubernetes/Helm](#kuberneteshelm)
|
||||
- [AWS Deployment](#aws-deployment)
|
||||
- [Self-Hosted Deployment](#self-hosted-deployment)
|
||||
- [GitLab CI/CD](#gitlab-cicd)
|
||||
|
||||
---
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.11+
|
||||
- PostgreSQL 15+
|
||||
- MinIO or AWS S3 access
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Create virtual environment:**
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Set up PostgreSQL:**
|
||||
```bash
|
||||
createdb datalake
|
||||
```
|
||||
|
||||
4. **Configure environment:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
5. **Run the application:**
|
||||
```bash
|
||||
python -m uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Start all services:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Check logs:**
|
||||
```bash
|
||||
docker-compose logs -f api
|
||||
```
|
||||
|
||||
3. **Stop services:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Services Included
|
||||
- PostgreSQL (port 5432)
|
||||
- MinIO (port 9000, console 9001)
|
||||
- API (port 8000)
|
||||
|
||||
### Customization
|
||||
|
||||
Edit `docker-compose.yml` to:
|
||||
- Change port mappings
|
||||
- Adjust resource limits
|
||||
- Add environment variables
|
||||
- Configure volumes
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes/Helm
|
||||
|
||||
### Prerequisites
|
||||
- Kubernetes cluster (1.24+)
|
||||
- Helm 3.x
|
||||
- kubectl configured
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Add dependencies (if using PostgreSQL/MinIO from Bitnami):**
|
||||
```bash
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm repo update
|
||||
```
|
||||
|
||||
2. **Install with default values:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
3. **Custom installation:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set image.repository=your-registry/datalake \
|
||||
--set image.tag=1.0.0 \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=datalake.yourdomain.com
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
**Image:**
|
||||
```bash
|
||||
--set image.repository=your-registry/datalake
|
||||
--set image.tag=1.0.0
|
||||
--set image.pullPolicy=Always
|
||||
```
|
||||
|
||||
**Resources:**
|
||||
```bash
|
||||
--set resources.requests.cpu=1000m
|
||||
--set resources.requests.memory=1Gi
|
||||
--set resources.limits.cpu=2000m
|
||||
--set resources.limits.memory=2Gi
|
||||
```
|
||||
|
||||
**Autoscaling:**
|
||||
```bash
|
||||
--set autoscaling.enabled=true
|
||||
--set autoscaling.minReplicas=3
|
||||
--set autoscaling.maxReplicas=10
|
||||
--set autoscaling.targetCPUUtilizationPercentage=80
|
||||
```
|
||||
|
||||
**Ingress:**
|
||||
```bash
|
||||
--set ingress.enabled=true
|
||||
--set ingress.className=nginx
|
||||
--set ingress.hosts[0].host=datalake.example.com
|
||||
--set ingress.hosts[0].paths[0].path=/
|
||||
--set ingress.hosts[0].paths[0].pathType=Prefix
|
||||
```
|
||||
|
||||
### Upgrade
|
||||
|
||||
```bash
|
||||
helm upgrade datalake ./helm \
|
||||
--namespace datalake \
|
||||
--set image.tag=1.1.0
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
helm uninstall datalake --namespace datalake
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AWS Deployment
|
||||
|
||||
### Using AWS S3 Storage
|
||||
|
||||
1. **Create S3 bucket:**
|
||||
```bash
|
||||
aws s3 mb s3://your-test-artifacts-bucket
|
||||
```
|
||||
|
||||
2. **Create IAM user with S3 access:**
|
||||
```bash
|
||||
aws iam create-user --user-name datalake-service
|
||||
aws iam attach-user-policy --user-name datalake-service \
|
||||
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
|
||||
```
|
||||
|
||||
3. **Generate access keys:**
|
||||
```bash
|
||||
aws iam create-access-key --user-name datalake-service
|
||||
```
|
||||
|
||||
4. **Deploy with Helm:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set config.storageBackend=s3 \
|
||||
--set aws.enabled=true \
|
||||
--set aws.accessKeyId=YOUR_ACCESS_KEY \
|
||||
--set aws.secretAccessKey=YOUR_SECRET_KEY \
|
||||
--set aws.region=us-east-1 \
|
||||
--set aws.bucketName=your-test-artifacts-bucket \
|
||||
--set minio.enabled=false
|
||||
```
|
||||
|
||||
### Using EKS
|
||||
|
||||
1. **Create EKS cluster:**
|
||||
```bash
|
||||
eksctl create cluster \
|
||||
--name datalake-cluster \
|
||||
--region us-east-1 \
|
||||
--nodegroup-name standard-workers \
|
||||
--node-type t3.medium \
|
||||
--nodes 3
|
||||
```
|
||||
|
||||
2. **Configure kubectl:**
|
||||
```bash
|
||||
aws eks update-kubeconfig --name datalake-cluster --region us-east-1
|
||||
```
|
||||
|
||||
3. **Deploy application:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set config.storageBackend=s3
|
||||
```
|
||||
|
||||
### Using RDS for PostgreSQL
|
||||
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set postgresql.enabled=false \
|
||||
--set config.databaseUrl="postgresql://user:pass@your-rds-endpoint:5432/datalake"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Hosted Deployment
|
||||
|
||||
### Using MinIO
|
||||
|
||||
1. **Deploy MinIO:**
|
||||
```bash
|
||||
helm install minio bitnami/minio \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set auth.rootUser=admin \
|
||||
--set auth.rootPassword=adminpassword \
|
||||
--set persistence.size=100Gi
|
||||
```
|
||||
|
||||
2. **Deploy application:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--set config.storageBackend=minio \
|
||||
--set minio.enabled=false \
|
||||
--set minio.endpoint=minio:9000 \
|
||||
--set minio.accessKey=admin \
|
||||
--set minio.secretKey=adminpassword
|
||||
```
|
||||
|
||||
### On-Premise Kubernetes
|
||||
|
||||
1. **Prepare persistent volumes:**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: datalake-postgres-pv
|
||||
spec:
|
||||
capacity:
|
||||
storage: 20Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: /data/postgres
|
||||
```
|
||||
|
||||
2. **Deploy with local storage:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--namespace datalake \
|
||||
--create-namespace \
|
||||
--set postgresql.persistence.storageClass=local-storage \
|
||||
--set minio.persistence.storageClass=local-storage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GitLab CI/CD
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Configure GitLab variables:**
|
||||
|
||||
Go to Settings → CI/CD → Variables and add:
|
||||
|
||||
| Variable | Description | Protected | Masked |
|
||||
|----------|-------------|-----------|---------|
|
||||
| `CI_REGISTRY_USER` | Docker registry username | No | No |
|
||||
| `CI_REGISTRY_PASSWORD` | Docker registry password | No | Yes |
|
||||
| `KUBE_CONFIG_DEV` | Base64 kubeconfig for dev | No | Yes |
|
||||
| `KUBE_CONFIG_STAGING` | Base64 kubeconfig for staging | Yes | Yes |
|
||||
| `KUBE_CONFIG_PROD` | Base64 kubeconfig for prod | Yes | Yes |
|
||||
|
||||
2. **Encode kubeconfig:**
|
||||
```bash
|
||||
cat ~/.kube/config | base64 -w 0
|
||||
```
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
1. **Test**: Runs on all branches and MRs
|
||||
2. **Build**: Builds Docker image on main/develop/tags
|
||||
3. **Deploy**: Manual deployment to dev/staging/prod
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
**Development:**
|
||||
```bash
|
||||
git push origin develop
|
||||
# Manually trigger deploy:dev job in GitLab
|
||||
```
|
||||
|
||||
**Staging:**
|
||||
```bash
|
||||
git push origin main
|
||||
# Manually trigger deploy:staging job in GitLab
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
# Manually trigger deploy:prod job in GitLab
|
||||
```
|
||||
|
||||
### Customizing Pipeline
|
||||
|
||||
Edit `.gitlab-ci.yml` to:
|
||||
- Add more test stages
|
||||
- Change deployment namespaces
|
||||
- Adjust Helm values per environment
|
||||
- Add security scanning
|
||||
- Configure rollback procedures
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Kubernetes
|
||||
kubectl get pods -n datalake
|
||||
kubectl logs -f -n datalake deployment/datalake
|
||||
|
||||
# Direct
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
Add Prometheus monitoring:
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--set metrics.enabled=true \
|
||||
--set serviceMonitor.enabled=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Database Backup
|
||||
|
||||
```bash
|
||||
# PostgreSQL
|
||||
kubectl exec -n datalake deployment/datalake-postgresql -- \
|
||||
pg_dump -U user datalake > backup.sql
|
||||
|
||||
# Restore
|
||||
kubectl exec -i -n datalake deployment/datalake-postgresql -- \
|
||||
psql -U user datalake < backup.sql
|
||||
```
|
||||
|
||||
### Storage Backup
|
||||
|
||||
**S3:**
|
||||
```bash
|
||||
aws s3 sync s3://your-bucket s3://backup-bucket
|
||||
```
|
||||
|
||||
**MinIO:**
|
||||
```bash
|
||||
mc mirror minio/test-artifacts backup/test-artifacts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pod Not Starting
|
||||
```bash
|
||||
kubectl describe pod -n datalake <pod-name>
|
||||
kubectl logs -n datalake <pod-name>
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
```bash
|
||||
kubectl exec -it -n datalake deployment/datalake -- \
|
||||
psql $DATABASE_URL
|
||||
```
|
||||
|
||||
### Storage Issues
|
||||
```bash
|
||||
# Check MinIO
|
||||
kubectl port-forward -n datalake svc/minio 9000:9000
|
||||
# Access http://localhost:9000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Use secrets management:**
|
||||
- Kubernetes Secrets
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
|
||||
2. **Enable TLS:**
|
||||
- Configure ingress with TLS certificates
|
||||
- Use cert-manager for automatic certificates
|
||||
|
||||
3. **Network policies:**
|
||||
- Restrict pod-to-pod communication
|
||||
- Limit external access
|
||||
|
||||
4. **RBAC:**
|
||||
- Configure Kubernetes RBAC
|
||||
- Limit service account permissions
|
||||
|
||||
---
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Database
|
||||
- Increase connection pool size
|
||||
- Add database indexes
|
||||
- Configure autovacuum
|
||||
|
||||
### API
|
||||
- Increase replica count
|
||||
- Configure horizontal pod autoscaling
|
||||
- Adjust resource requests/limits
|
||||
|
||||
### Storage
|
||||
- Use CDN for frequently accessed files
|
||||
- Configure S3 Transfer Acceleration
|
||||
- Optimize MinIO deployment
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,3 +1,30 @@
|
||||
# Multi-stage build: First stage builds Angular frontend
|
||||
FROM node:20.11-alpine3.19 AS frontend-build
|
||||
|
||||
# Accept npm registry as build argument
|
||||
ARG NPM_REGISTRY=https://registry.npmjs.org/
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
# Copy package files
|
||||
COPY frontend/package*.json ./
|
||||
|
||||
# Configure npm registry if custom registry is provided
|
||||
RUN if [ "$NPM_REGISTRY" != "https://registry.npmjs.org/" ]; then \
|
||||
echo "Using custom npm registry: $NPM_REGISTRY"; \
|
||||
npm config set registry "$NPM_REGISTRY"; \
|
||||
fi
|
||||
|
||||
# Install dependencies (ignore package-lock.json if using custom registry)
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY frontend/ ./
|
||||
|
||||
# Build for production
|
||||
RUN npm run build:prod
|
||||
|
||||
# Second stage: Python backend with Angular frontend
|
||||
FROM python:3.11-alpine
|
||||
|
||||
WORKDIR /app
|
||||
@@ -20,7 +47,9 @@ COPY app/ ./app/
|
||||
COPY utils/ ./utils/
|
||||
COPY alembic/ ./alembic/
|
||||
COPY alembic.ini .
|
||||
COPY static/ ./static/
|
||||
|
||||
# Copy built Angular frontend from first stage to static directory
|
||||
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
|
||||
|
||||
# Create non-root user (Alpine uses adduser instead of useradd)
|
||||
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
|
||||
|
||||
74
README.md
74
README.md
@@ -1,4 +1,4 @@
|
||||
# Obsidian
|
||||
# Warehouse13
|
||||
|
||||
**Enterprise Test Artifact Storage**
|
||||
|
||||
@@ -36,7 +36,7 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
|
||||
|
||||
## Quick Start
|
||||
|
||||
### One-Command Setup
|
||||
### Standard Deployment (Internet Access)
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
@@ -48,11 +48,20 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
|
||||
.\quickstart.ps1
|
||||
```
|
||||
|
||||
**Windows (Command Prompt):**
|
||||
```batch
|
||||
quickstart.bat
|
||||
### Air-Gapped/Restricted Environment Deployment
|
||||
|
||||
**For environments with restricted npm access:**
|
||||
```bash
|
||||
./quickstart-airgap.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Builds Angular locally (where npm works)
|
||||
2. Packages pre-built files into Docker
|
||||
3. Starts all services
|
||||
|
||||
See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions.
|
||||
|
||||
### Manual Setup with Docker Compose
|
||||
|
||||
1. Clone the repository:
|
||||
@@ -205,35 +214,54 @@ MINIO_BUCKET_NAME=test-artifacts
|
||||
|
||||
### Kubernetes with Helm
|
||||
|
||||
1. Build and push Docker image:
|
||||
**Quick Start:**
|
||||
```bash
|
||||
docker build -t your-registry/datalake:latest .
|
||||
docker push your-registry/datalake:latest
|
||||
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
||||
```
|
||||
|
||||
2. Install with Helm:
|
||||
**Production Deployment:**
|
||||
```bash
|
||||
helm install datalake ./helm \
|
||||
--set image.repository=your-registry/datalake \
|
||||
--set image.tag=latest \
|
||||
--namespace datalake \
|
||||
--create-namespace
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--create-namespace \
|
||||
--values ./helm/warehouse13/values-production.yaml
|
||||
```
|
||||
|
||||
3. Access the API:
|
||||
**Air-Gapped Deployment:**
|
||||
```bash
|
||||
kubectl port-forward -n datalake svc/datalake 8000:8000
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--create-namespace \
|
||||
--values ./helm/warehouse13/values-airgapped.yaml
|
||||
```
|
||||
|
||||
**Access the Application:**
|
||||
```bash
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||
```
|
||||
|
||||
### Helm Documentation
|
||||
|
||||
- **Full Helm Guide:** [HELM-DEPLOYMENT.md](./docs/HELM-DEPLOYMENT.md)
|
||||
- **Chart README:** [helm/warehouse13/README.md](./helm/warehouse13/README.md)
|
||||
- **Quick Start:** [helm/warehouse13/QUICKSTART.md](./helm/warehouse13/QUICKSTART.md)
|
||||
- **Example Configurations:**
|
||||
- Development: [values-dev.yaml](./helm/warehouse13/values-dev.yaml)
|
||||
- Production: [values-production.yaml](./helm/warehouse13/values-production.yaml)
|
||||
- Air-Gapped: [values-airgapped.yaml](./helm/warehouse13/values-airgapped.yaml)
|
||||
|
||||
### Helm Configuration
|
||||
|
||||
Edit `helm/values.yaml` to customize:
|
||||
- Replica count
|
||||
- Resource limits
|
||||
- Storage backend (S3 vs MinIO)
|
||||
- Ingress settings
|
||||
- PostgreSQL settings
|
||||
- Autoscaling
|
||||
All component images are fully configurable in `helm/warehouse13/values.yaml`:
|
||||
- PostgreSQL image and version
|
||||
- MinIO image and version
|
||||
- API image and version
|
||||
- Frontend image and version
|
||||
- Resource limits and requests
|
||||
- Storage backend configuration
|
||||
- Ingress and TLS settings
|
||||
- Persistence and storage classes
|
||||
|
||||
### GitLab CI/CD
|
||||
|
||||
|
||||
36
app/main.py
36
app/main.py
@@ -1,4 +1,4 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Obsidian",
|
||||
title="Warehouse13",
|
||||
description="Enterprise Test Artifact Storage - API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
@@ -39,10 +39,8 @@ app.add_middleware(
|
||||
app.include_router(artifacts_router)
|
||||
app.include_router(seed_router)
|
||||
|
||||
# Mount static files
|
||||
# Static directory setup
|
||||
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
||||
if os.path.exists(static_dir):
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -59,7 +57,7 @@ async def startup_event():
|
||||
async def api_root():
|
||||
"""API root endpoint"""
|
||||
return {
|
||||
"message": "Obsidian - Enterprise Test Artifact Storage",
|
||||
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"deployment_mode": settings.deployment_mode,
|
||||
@@ -75,7 +73,31 @@ async def ui_root():
|
||||
return FileResponse(index_path)
|
||||
else:
|
||||
return {
|
||||
"message": "Obsidian - Enterprise Test Artifact Storage",
|
||||
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"ui": "UI not found. Serving API only.",
|
||||
"deployment_mode": settings.deployment_mode,
|
||||
"storage_backend": settings.storage_backend
|
||||
}
|
||||
|
||||
|
||||
# Catch-all route for Angular SPA routing - must be last
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_spa(full_path: str):
|
||||
"""Serve Angular SPA static files and handle client-side routing"""
|
||||
# Try to serve static file first (JS, CSS, images, etc.)
|
||||
file_path = os.path.join(static_dir, full_path)
|
||||
if os.path.exists(file_path) and os.path.isfile(file_path):
|
||||
return FileResponse(file_path)
|
||||
|
||||
# For all other routes (Angular client-side routes), serve index.html
|
||||
index_path = os.path.join(static_dir, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
return FileResponse(index_path)
|
||||
else:
|
||||
return {
|
||||
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"ui": "UI not found. Serving API only.",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
version: '3.8'
|
||||
|
||||
name: warehouse13
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
@@ -34,8 +36,12 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
build: .
|
||||
app:
|
||||
container_name: warehouse13-app
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
|
||||
140
docs/DEPLOYMENT.md
Normal file
140
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Deployment Options
|
||||
|
||||
This project supports two deployment strategies for the Angular frontend, depending on your environment's network access.
|
||||
|
||||
## Option 1: Standard Build (Internet Access Required)
|
||||
|
||||
Use the standard `Dockerfile.frontend` which builds the Angular app inside Docker.
|
||||
|
||||
**Requirements:**
|
||||
- Internet access to npm registry
|
||||
- Docker build environment
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./quickstart.sh
|
||||
# or
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
This uses `Dockerfile.frontend` which:
|
||||
1. Installs npm dependencies in Docker
|
||||
2. Builds Angular app in Docker
|
||||
3. Serves with nginx
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments) ⭐ RECOMMENDED
|
||||
|
||||
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access.
|
||||
|
||||
**Requirements:**
|
||||
- Node.js 18+ installed locally (on a machine with npm access)
|
||||
- npm installed locally
|
||||
- No internet required during Docker build
|
||||
|
||||
**Note:** This project uses Angular 17 with webpack bundler (not Vite) for better compatibility with restricted npm environments.
|
||||
|
||||
**Usage:**
|
||||
|
||||
### Quick Start (Recommended)
|
||||
```bash
|
||||
./quickstart-airgap.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Build the Angular app locally
|
||||
2. Start all Docker containers
|
||||
3. Verify the deployment
|
||||
|
||||
### Manual Steps
|
||||
|
||||
### Step 1: Build Angular app locally
|
||||
**IMPORTANT:** You MUST run this step BEFORE `docker-compose up`!
|
||||
|
||||
```bash
|
||||
# Option A: Use the helper script
|
||||
./scripts/build-for-airgap.sh
|
||||
|
||||
# Option B: Build manually
|
||||
cd frontend
|
||||
npm install # Only needed once or when dependencies change
|
||||
npm run build:prod
|
||||
cd ..
|
||||
```
|
||||
|
||||
This creates `frontend/dist/frontend/browser/` which Docker will copy.
|
||||
|
||||
### Step 2: Update docker-compose.yml
|
||||
Edit `docker-compose.yml` and change the frontend dockerfile:
|
||||
|
||||
```yaml
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.frontend.prebuilt # <-- Change this line
|
||||
ports:
|
||||
- "4200:80"
|
||||
depends_on:
|
||||
- api
|
||||
```
|
||||
|
||||
### Step 3: Build and deploy
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
This uses `Dockerfile.frontend.prebuilt` which:
|
||||
1. Copies pre-built Angular files from `frontend/dist/`
|
||||
2. Serves with nginx
|
||||
3. No npm/node required in Docker
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Tool Package Issues
|
||||
|
||||
If you see errors about missing packages like:
|
||||
```
|
||||
Cannot find package "vite"
|
||||
Cannot find package "esbuild"
|
||||
Cannot find package "rollup"
|
||||
```
|
||||
|
||||
**Solution:** This project uses Angular 17 with webpack bundler specifically to avoid these issues. If you still encounter package access problems in your restricted environment, use Option 2 (Pre-built) deployment above, which eliminates all npm dependencies in Docker.
|
||||
|
||||
### Custom NPM Registry
|
||||
|
||||
For both options, you can use a custom npm registry:
|
||||
|
||||
```bash
|
||||
# Set in .env file
|
||||
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/
|
||||
|
||||
# Or inline
|
||||
NPM_REGISTRY=http://your-proxy ./quickstart.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
- **Development/Cloud**: Use Option 1 (standard)
|
||||
- **Air-gapped/Enterprise**: Use Option 2 (pre-built) ⭐ **RECOMMENDED**
|
||||
- **CI/CD**: Use Option 2 for faster, more reliable builds
|
||||
- **Restricted npm access**: Use Option 2 (pre-built) ⭐ **REQUIRED**
|
||||
|
||||
---
|
||||
|
||||
## Build Strategy for Restricted Environments
|
||||
|
||||
**This project uses Angular 17 with webpack** instead of Angular 19 with Vite specifically for better compatibility with restricted npm environments. Webpack has fewer platform-specific binary dependencies than Vite.
|
||||
|
||||
If you encounter any package access errors during builds:
|
||||
- `Cannot find package "vite"`
|
||||
- `Cannot find package "rollup"`
|
||||
- `Cannot find package "esbuild"`
|
||||
- Any platform-specific binary errors
|
||||
|
||||
**Solution:** Use Option 2 (Pre-built) deployment. This completely avoids npm installation in Docker and eliminates all build tool dependency issues.
|
||||
517
docs/HELM-DEPLOYMENT.md
Normal file
517
docs/HELM-DEPLOYMENT.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# Warehouse13 - Kubernetes Deployment with Helm
|
||||
|
||||
This guide covers deploying Warehouse13 to Kubernetes using the official Helm chart.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Quick Start](#quick-start)
|
||||
3. [Deployment Scenarios](#deployment-scenarios)
|
||||
4. [Configuration](#configuration)
|
||||
5. [Post-Deployment](#post-deployment)
|
||||
6. [Upgrading](#upgrading)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.19+ cluster
|
||||
- Helm 3.0+
|
||||
- kubectl configured to access your cluster
|
||||
- Persistent volume provisioner (for production deployments)
|
||||
|
||||
### Installing Helm
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install helm
|
||||
|
||||
# Linux
|
||||
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
# Windows
|
||||
choco install kubernetes-helm
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Standard Deployment (Internet Access)
|
||||
|
||||
```bash
|
||||
# Create namespace
|
||||
kubectl create namespace warehouse13
|
||||
|
||||
# Install with default values
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13
|
||||
|
||||
# Wait for pods to be ready
|
||||
kubectl wait --for=condition=ready pod \
|
||||
--all --namespace warehouse13 --timeout=300s
|
||||
```
|
||||
|
||||
### 2. Access the Application
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||
|
||||
# API
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||
|
||||
# MinIO Console
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||
```
|
||||
|
||||
Then visit:
|
||||
- Frontend: http://localhost:4200
|
||||
- API Docs: http://localhost:8000/docs
|
||||
- MinIO Console: http://localhost:9001
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
### Development Environment
|
||||
|
||||
For local testing or CI/CD:
|
||||
|
||||
```bash
|
||||
helm install warehouse13-dev ./helm/warehouse13 \
|
||||
--namespace warehouse13-dev \
|
||||
--create-namespace \
|
||||
--values ./helm/warehouse13/values-dev.yaml
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Single replica for all services
|
||||
- emptyDir storage (no persistence)
|
||||
- Minimal resource requests
|
||||
- Always pull latest dev images
|
||||
|
||||
### Production Environment
|
||||
|
||||
For production with ingress and high availability:
|
||||
|
||||
```bash
|
||||
# First, update the values file with your domain and secrets
|
||||
cp ./helm/warehouse13/values-production.yaml ./my-production-values.yaml
|
||||
|
||||
# Edit the file:
|
||||
# - Set postgres.auth.password
|
||||
# - Set minio.auth.rootUser and rootPassword
|
||||
# - Set ingress.hosts[0].host to your domain
|
||||
# - Update storageClass for your environment
|
||||
|
||||
# Install
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--create-namespace \
|
||||
--values ./my-production-values.yaml
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- 3 replicas for API and frontend
|
||||
- Persistent storage with PVCs
|
||||
- Ingress with TLS support
|
||||
- Resource limits and requests
|
||||
- Health checks enabled
|
||||
- Pod anti-affinity for distribution
|
||||
|
||||
### Air-Gapped Environment
|
||||
|
||||
For restricted/disconnected environments:
|
||||
|
||||
```bash
|
||||
# 1. First, push images to your internal registry
|
||||
# Example using harbor.internal.example.com
|
||||
|
||||
# Pull images (on internet-connected machine)
|
||||
docker pull postgres:15-alpine
|
||||
docker pull minio/minio:latest
|
||||
docker pull warehouse13/api:v1.0.0
|
||||
docker pull warehouse13/frontend:v1.0.0
|
||||
|
||||
# Tag for internal registry
|
||||
docker tag postgres:15-alpine harbor.internal.example.com/library/postgres:15-alpine
|
||||
docker tag minio/minio:latest harbor.internal.example.com/library/minio:latest
|
||||
docker tag warehouse13/api:v1.0.0 harbor.internal.example.com/warehouse13/api:v1.0.0
|
||||
docker tag warehouse13/frontend:v1.0.0 harbor.internal.example.com/warehouse13/frontend:v1.0.0
|
||||
|
||||
# Push to internal registry
|
||||
docker push harbor.internal.example.com/library/postgres:15-alpine
|
||||
docker push harbor.internal.example.com/library/minio:latest
|
||||
docker push harbor.internal.example.com/warehouse13/api:v1.0.0
|
||||
docker push harbor.internal.example.com/warehouse13/frontend:v1.0.0
|
||||
|
||||
# 2. Update the values file with your registry
|
||||
cp ./helm/warehouse13/values-airgapped.yaml ./my-airgapped-values.yaml
|
||||
|
||||
# Edit to match your environment:
|
||||
# - Update all image.repository values
|
||||
# - Set secure passwords
|
||||
# - Configure storage classes
|
||||
# - Add node selectors/tolerations if needed
|
||||
|
||||
# 3. Install on air-gapped cluster
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--create-namespace \
|
||||
--values ./my-airgapped-values.yaml
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- All images from custom registry
|
||||
- Local storage class support
|
||||
- Node selectors for specific nodes
|
||||
- Tolerations for tainted nodes
|
||||
|
||||
## Configuration
|
||||
|
||||
### Configurable Images
|
||||
|
||||
All component images can be customized:
|
||||
|
||||
```yaml
|
||||
# PostgreSQL
|
||||
postgres:
|
||||
image:
|
||||
repository: postgres # or your-registry/postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# MinIO
|
||||
minio:
|
||||
image:
|
||||
repository: minio/minio # or your-registry/minio
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# API Backend
|
||||
api:
|
||||
image:
|
||||
repository: warehouse13/api # or your-registry/warehouse13-api
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Frontend
|
||||
frontend:
|
||||
image:
|
||||
repository: warehouse13/frontend # or your-registry/warehouse13-frontend
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
```
|
||||
|
||||
### Quick Image Override
|
||||
|
||||
```bash
|
||||
# Override images from command line
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--set postgres.image.repository=myregistry.com/postgres \
|
||||
--set postgres.image.tag=15-alpine \
|
||||
--set minio.image.repository=myregistry.com/minio \
|
||||
--set minio.image.tag=latest \
|
||||
--set api.image.repository=myregistry.com/warehouse13-api \
|
||||
--set api.image.tag=v1.0.0 \
|
||||
--set frontend.image.repository=myregistry.com/warehouse13-frontend \
|
||||
--set frontend.image.tag=v1.0.0
|
||||
```
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
```yaml
|
||||
# PostgreSQL storage
|
||||
postgres:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 50Gi
|
||||
storageClass: "fast-ssd" # or "" for default
|
||||
|
||||
# MinIO storage
|
||||
minio:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 500Gi
|
||||
storageClass: "bulk-storage" # or "" for default
|
||||
```
|
||||
|
||||
### Resource Configuration
|
||||
|
||||
```yaml
|
||||
# API resources
|
||||
api:
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
# Frontend resources
|
||||
frontend:
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
```
|
||||
|
||||
### Ingress Configuration
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: warehouse13.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: frontend
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend: api
|
||||
tls:
|
||||
- secretName: warehouse13-tls
|
||||
hosts:
|
||||
- warehouse13.example.com
|
||||
```
|
||||
|
||||
## Post-Deployment
|
||||
|
||||
### Verify Installation
|
||||
|
||||
```bash
|
||||
# Check all pods are running
|
||||
kubectl get pods -n warehouse13
|
||||
|
||||
# Check services
|
||||
kubectl get svc -n warehouse13
|
||||
|
||||
# Check PVCs
|
||||
kubectl get pvc -n warehouse13
|
||||
|
||||
# Check ingress (if enabled)
|
||||
kubectl get ingress -n warehouse13
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# API logs
|
||||
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=100 -f
|
||||
|
||||
# Frontend logs
|
||||
kubectl logs -n warehouse13 -l app.kubernetes.io/component=frontend --tail=100 -f
|
||||
|
||||
# PostgreSQL logs
|
||||
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100 -f
|
||||
|
||||
# MinIO logs
|
||||
kubectl logs -n warehouse13 warehouse13-minio-0 --tail=100 -f
|
||||
```
|
||||
|
||||
### Initialize MinIO Bucket
|
||||
|
||||
```bash
|
||||
# Port-forward to MinIO console
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||
|
||||
# Open http://localhost:9001
|
||||
# Login with credentials from values.yaml
|
||||
# Create bucket: "artifacts"
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Upgrade to New Version
|
||||
|
||||
```bash
|
||||
# Update image tags in values file
|
||||
# Then run upgrade
|
||||
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--values ./my-production-values.yaml \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
|
||||
# Check rollout status
|
||||
kubectl rollout status deployment/warehouse13-api -n warehouse13
|
||||
kubectl rollout status deployment/warehouse13-frontend -n warehouse13
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
# View revision history
|
||||
helm history warehouse13 -n warehouse13
|
||||
|
||||
# Rollback to previous version
|
||||
helm rollback warehouse13 -n warehouse13
|
||||
|
||||
# Rollback to specific revision
|
||||
helm rollback warehouse13 2 -n warehouse13
|
||||
```
|
||||
|
||||
### Update Values Only
|
||||
|
||||
```bash
|
||||
# Update configuration without changing images
|
||||
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--values ./my-updated-values.yaml \
|
||||
--reuse-values
|
||||
```
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### PostgreSQL Backup
|
||||
|
||||
```bash
|
||||
# Create backup
|
||||
kubectl exec -n warehouse13 warehouse13-postgres-0 -- \
|
||||
pg_dump -U warehouse13user warehouse13 > backup-$(date +%Y%m%d).sql
|
||||
|
||||
# Restore
|
||||
cat backup-20241016.sql | kubectl exec -i -n warehouse13 warehouse13-postgres-0 -- \
|
||||
psql -U warehouse13user warehouse13
|
||||
```
|
||||
|
||||
### MinIO Backup
|
||||
|
||||
```bash
|
||||
# Install MinIO Client
|
||||
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
chmod +x mc
|
||||
|
||||
# Configure
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9000:9000
|
||||
mc alias set w13 http://localhost:9000 <access-key> <secret-key>
|
||||
|
||||
# Backup bucket
|
||||
mc mirror w13/artifacts ./backup/artifacts-$(date +%Y%m%d)
|
||||
|
||||
# Restore
|
||||
mc mirror ./backup/artifacts-20241016 w13/artifacts
|
||||
```
|
||||
|
||||
### Full Backup
|
||||
|
||||
```bash
|
||||
# Backup all PVCs
|
||||
for pvc in $(kubectl get pvc -n warehouse13 -o name); do
|
||||
pvc_name=$(basename $pvc)
|
||||
kubectl get -n warehouse13 $pvc -o yaml > backup-${pvc_name}.yaml
|
||||
done
|
||||
|
||||
# Backup Helm values
|
||||
helm get values warehouse13 -n warehouse13 > backup-values.yaml
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pods Not Starting
|
||||
|
||||
```bash
|
||||
# Check pod status
|
||||
kubectl get pods -n warehouse13
|
||||
|
||||
# Describe pod for events
|
||||
kubectl describe pod <pod-name> -n warehouse13
|
||||
|
||||
# Check logs
|
||||
kubectl logs <pod-name> -n warehouse13
|
||||
|
||||
# Common issues:
|
||||
# - ImagePullBackOff: Check image repository and credentials
|
||||
# - Pending: Check PVC status and node resources
|
||||
# - CrashLoopBackOff: Check application logs
|
||||
```
|
||||
|
||||
### PVC Issues
|
||||
|
||||
```bash
|
||||
# Check PVC status
|
||||
kubectl get pvc -n warehouse13
|
||||
|
||||
# Describe PVC
|
||||
kubectl describe pvc <pvc-name> -n warehouse13
|
||||
|
||||
# Common issues:
|
||||
# - Pending: No storage class or insufficient storage
|
||||
# - Bound: PVC is healthy
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
```bash
|
||||
# Test PostgreSQL connection
|
||||
kubectl exec -it -n warehouse13 warehouse13-postgres-0 -- \
|
||||
psql -U warehouse13user -d warehouse13
|
||||
|
||||
# Check database logs
|
||||
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100
|
||||
|
||||
# Verify secret
|
||||
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
|
||||
```
|
||||
|
||||
### Ingress Not Working
|
||||
|
||||
```bash
|
||||
# Check ingress status
|
||||
kubectl get ingress -n warehouse13
|
||||
kubectl describe ingress -n warehouse13 warehouse13-ingress
|
||||
|
||||
# Check ingress controller logs
|
||||
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller
|
||||
|
||||
# Verify TLS certificate
|
||||
kubectl get certificate -n warehouse13
|
||||
kubectl describe certificate -n warehouse13 warehouse13-tls
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
```bash
|
||||
# Check resource usage
|
||||
kubectl top pods -n warehouse13
|
||||
kubectl top nodes
|
||||
|
||||
# Check if pods are being throttled
|
||||
kubectl describe pod <pod-name> -n warehouse13 | grep -A 5 "State:"
|
||||
|
||||
# Increase resources
|
||||
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||
--set api.resources.limits.memory=2Gi \
|
||||
--set api.resources.limits.cpu=2000m
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
# Uninstall the release
|
||||
helm uninstall warehouse13 -n warehouse13
|
||||
|
||||
# Delete PVCs (data will be lost!)
|
||||
kubectl delete pvc -n warehouse13 -l app.kubernetes.io/instance=warehouse13
|
||||
|
||||
# Delete namespace
|
||||
kubectl delete namespace warehouse13
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Helm Chart README](./helm/warehouse13/README.md)
|
||||
- [Values Documentation](./helm/warehouse13/values.yaml)
|
||||
- [Docker Deployment Guide](./DEPLOYMENT.md)
|
||||
- [Main README](./README.md)
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||
- Helm Chart Issues: Tag with `helm` label
|
||||
220
docs/NPM-PACKAGE-AGE-POLICY.md
Normal file
220
docs/NPM-PACKAGE-AGE-POLICY.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# NPM Package Age Policy
|
||||
|
||||
## Requirement
|
||||
|
||||
All npm packages must be **at least 2 weeks old** before they can be used in this project. This ensures:
|
||||
- Package stability
|
||||
- Security vulnerability disclosure time
|
||||
- Compliance with organizational security policies
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Package Version Pinning
|
||||
|
||||
The project uses exact version pinning in `package.json` to prevent automatic updates:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@angular/core": "19.2.7", // Exact version, not "^19.2.7" or "~19.2.7"
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `frontend/.npmrc` file enforces this:
|
||||
```
|
||||
save-exact=true
|
||||
package-lock=true
|
||||
```
|
||||
|
||||
### 2. Automated Age Checking
|
||||
|
||||
Use the provided script to verify all packages meet the 2-week requirement:
|
||||
|
||||
```bash
|
||||
# Check if all packages are at least 2 weeks old
|
||||
node scripts/check-package-age.js
|
||||
```
|
||||
|
||||
This script:
|
||||
- Queries npm registry for publish dates
|
||||
- Calculates age of each package
|
||||
- Fails if any package is newer than 14 days
|
||||
- Shows detailed age information for all packages
|
||||
|
||||
### 3. Installation Process
|
||||
|
||||
**Always use `npm ci` instead of `npm install`:**
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm ci # Installs exact versions from package-lock.json
|
||||
```
|
||||
|
||||
**Why `npm ci`?**
|
||||
- Uses exact versions from `package-lock.json`
|
||||
- Doesn't update `package-lock.json`
|
||||
- Ensures reproducible builds
|
||||
- Faster than `npm install`
|
||||
|
||||
## Updating Packages
|
||||
|
||||
When you need to add or update packages:
|
||||
|
||||
### Step 1: Add Package to package.json
|
||||
|
||||
```bash
|
||||
# Find a version that's at least 2 weeks old
|
||||
npm view <package-name> time
|
||||
|
||||
# Add exact version to package.json
|
||||
"dependencies": {
|
||||
"new-package": "1.2.3"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Verify Age
|
||||
|
||||
```bash
|
||||
node scripts/check-package-age.js
|
||||
```
|
||||
|
||||
### Step 3: Update Lock File
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install --package-lock-only
|
||||
```
|
||||
|
||||
### Step 4: Install and Test
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Add the age check to your CI/CD pipeline:
|
||||
|
||||
### GitLab CI Example
|
||||
|
||||
```yaml
|
||||
verify_package_age:
|
||||
stage: validate
|
||||
image: node:18-alpine
|
||||
script:
|
||||
- node scripts/check-package-age.js
|
||||
only:
|
||||
- merge_requests
|
||||
- main
|
||||
```
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
- name: Check Package Age
|
||||
run: node scripts/check-package-age.js
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Package is too new" Error
|
||||
|
||||
If a package fails the age check:
|
||||
|
||||
1. **Find an older version:**
|
||||
```bash
|
||||
npm view <package-name> versions --json
|
||||
npm view <package-name>@<older-version> time
|
||||
```
|
||||
|
||||
2. **Update package.json with older version**
|
||||
|
||||
3. **Re-run age check:**
|
||||
```bash
|
||||
node scripts/check-package-age.js
|
||||
```
|
||||
|
||||
### Can't Find Old Enough Version
|
||||
|
||||
If no version meets the 2-week requirement:
|
||||
- Wait until the package is at least 2 weeks old
|
||||
- Look for alternative packages
|
||||
- Request an exception through your security team
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# 1. Check current package ages
|
||||
node scripts/check-package-age.js
|
||||
|
||||
# 2. If all pass, install dependencies
|
||||
cd frontend
|
||||
npm ci
|
||||
|
||||
# 3. Build application
|
||||
npm run build:prod
|
||||
|
||||
# 4. For air-gapped deployment
|
||||
../scripts/build-for-airgap.sh
|
||||
```
|
||||
|
||||
## Scripts Reference
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/check-package-age.js` | Verify all packages are ≥ 2 weeks old |
|
||||
| `scripts/pin-old-versions.sh` | Helper script to validate and pin versions |
|
||||
| `scripts/build-for-airgap.sh` | Build frontend for air-gapped deployment |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always commit `package-lock.json`**
|
||||
- Ensures everyone uses the same versions
|
||||
- Required for reproducible builds
|
||||
|
||||
2. **Use `npm ci` in CI/CD**
|
||||
- Faster than `npm install`
|
||||
- Enforces lock file versions
|
||||
- Prevents surprises
|
||||
|
||||
3. **Regular audits**
|
||||
```bash
|
||||
# Check for security vulnerabilities
|
||||
npm audit
|
||||
|
||||
# Check package ages
|
||||
node scripts/check-package-age.js
|
||||
```
|
||||
|
||||
4. **Version ranges to avoid**
|
||||
- ❌ `^1.2.3` (allows minor/patch updates)
|
||||
- ❌ `~1.2.3` (allows patch updates)
|
||||
- ❌ `*` or `latest` (allows any version)
|
||||
- ✅ `1.2.3` (exact version only)
|
||||
|
||||
## Package Age Check Output
|
||||
|
||||
```
|
||||
Checking package ages (must be at least 2 weeks old)...
|
||||
|
||||
✓ @angular/common@19.2.7 - 45 days old
|
||||
✓ @angular/compiler@19.2.7 - 45 days old
|
||||
✓ rxjs@7.8.0 - 180 days old
|
||||
❌ new-package@1.0.0 - 5 days old (published 2025-01-12)
|
||||
|
||||
================================================================================
|
||||
|
||||
❌ FAILED: 1 package(s) are newer than 2 weeks:
|
||||
|
||||
- new-package@1.0.0 (5 days old, published 2025-01-12)
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For questions or exceptions:
|
||||
- Review with security team
|
||||
- Document in project README
|
||||
- Update this policy as needed
|
||||
@@ -164,7 +164,7 @@ curl -X POST "http://localhost:8000/api/v1/artifacts/query" \
|
||||
make deploy
|
||||
|
||||
# Or directly with Helm
|
||||
helm install datalake ./helm --namespace datalake --create-namespace
|
||||
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
||||
```
|
||||
|
||||
## Feature Flags Usage
|
||||
@@ -190,9 +190,8 @@ AWS_REGION=us-east-1
|
||||
S3_BUCKET_NAME=your-bucket
|
||||
|
||||
# Deploy
|
||||
helm install datalake ./helm \
|
||||
--set config.deploymentMode=cloud \
|
||||
--set aws.enabled=true
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--set global.deploymentMode=cloud
|
||||
```
|
||||
|
||||
## What's Next
|
||||
17
frontend/.editorconfig
Normal file
17
frontend/.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
43
frontend/.gitignore
vendored
Normal file
43
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
__screenshots__/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
178
frontend/README.md
Normal file
178
frontend/README.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Warehouse13 Frontend - Angular Application
|
||||
|
||||
Modern Angular application for the Warehouse13 Test Artifact Data Lake.
|
||||
|
||||
## Features
|
||||
|
||||
- **Angular 20** with standalone components
|
||||
- **TypeScript** for type safety
|
||||
- **Reactive Forms** for upload and query functionality
|
||||
- **RxJS** for reactive programming
|
||||
- **Auto-refresh** artifacts every 5 seconds
|
||||
- **Client-side sorting and filtering**
|
||||
- **Dark theme** UI
|
||||
- **Responsive design**
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 24.x or higher
|
||||
- npm 11.x or higher
|
||||
- Backend API running on port 8000
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run Development Server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:4200/`
|
||||
|
||||
The development server includes a proxy configuration that forwards `/api` requests to `http://localhost:8000`.
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Build artifacts will be in the `dist/frontend/browser` directory.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── components/
|
||||
│ │ ├── artifacts-list/ # Main artifacts table with sorting, filtering, auto-refresh
|
||||
│ │ ├── upload-form/ # Reactive form for uploading artifacts
|
||||
│ │ └── query-form/ # Advanced query interface
|
||||
│ ├── models/
|
||||
│ │ └── artifact.model.ts # TypeScript interfaces for type safety
|
||||
│ ├── services/
|
||||
│ │ └── artifact.ts # HTTP service for API calls
|
||||
│ ├── app.ts # Main app component with routing
|
||||
│ ├── app.config.ts # Application configuration
|
||||
│ └── app.routes.ts # Route definitions
|
||||
├── styles.css # Global dark theme styles
|
||||
└── main.ts # Application bootstrap
|
||||
|
||||
## Key Components
|
||||
|
||||
### ArtifactsListComponent
|
||||
- Displays artifacts in a sortable, filterable table
|
||||
- Auto-refreshes every 5 seconds (toggleable)
|
||||
- Client-side search across all fields
|
||||
- Download and delete actions
|
||||
- Detail modal for full artifact information
|
||||
- Tags displayed as inline badges
|
||||
- SIM source grouping support
|
||||
|
||||
### UploadFormComponent
|
||||
- Reactive form with validation
|
||||
- File upload with drag-and-drop support
|
||||
- Required fields: File, Sim Source, Uploaded By, Tags
|
||||
- Optional fields: SIM Source ID (for grouping), Test Result, Version, Description, Test Config, Custom Metadata
|
||||
- JSON validation for config fields
|
||||
- Real-time upload status feedback
|
||||
|
||||
### QueryFormComponent
|
||||
- Advanced search with multiple filter criteria
|
||||
- Filter by: filename, file type, test name, test suite, test result, SIM source ID, tags, date range
|
||||
- Results emitted to artifacts list
|
||||
|
||||
## API Integration
|
||||
|
||||
The frontend communicates with the FastAPI backend through the `ArtifactService`:
|
||||
|
||||
- `GET /api/v1/artifacts/` - List all artifacts
|
||||
- `GET /api/v1/artifacts/:id` - Get single artifact
|
||||
- `POST /api/v1/artifacts/upload` - Upload new artifact
|
||||
- `POST /api/v1/artifacts/query` - Query with filters
|
||||
- `GET /api/v1/artifacts/:id/download` - Download artifact file
|
||||
- `DELETE /api/v1/artifacts/:id` - Delete artifact
|
||||
- `POST /api/v1/seed/generate/:count` - Generate seed data
|
||||
|
||||
## Configuration
|
||||
|
||||
### Proxy Configuration (`proxy.conf.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This proxies all `/api` requests to the backend during development.
|
||||
|
||||
## Styling
|
||||
|
||||
The application uses a custom dark theme with:
|
||||
- Dark blue/slate color palette
|
||||
- Gradient headers
|
||||
- Responsive design
|
||||
- Smooth transitions and hover effects
|
||||
- Tag badges for categorization
|
||||
- Result badges for test statuses
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
|
||||
## Development Tips
|
||||
|
||||
1. **Hot Reload**: Changes to TypeScript files automatically trigger recompilation
|
||||
2. **Type Safety**: Use TypeScript interfaces in `models/` for all API responses
|
||||
3. **State Management**: Currently using component-level state; consider NgRx for complex state
|
||||
4. **Testing**: Run `npm test` for unit tests (Jasmine/Karma)
|
||||
|
||||
## Deployment
|
||||
|
||||
For production deployment, build the application and serve the `dist/frontend/browser` directory with your web server (nginx, Apache, etc.).
|
||||
|
||||
Example nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /path/to/dist/frontend/browser;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Add NgRx for state management
|
||||
- [ ] Implement WebSocket for real-time updates
|
||||
- [ ] Add Angular Material components
|
||||
- [ ] Unit and E2E tests
|
||||
- [ ] PWA support
|
||||
- [ ] Drag-and-drop file upload
|
||||
- [ ] Bulk operations
|
||||
- [ ] Export to CSV/JSON
|
||||
|
||||
## License
|
||||
|
||||
Same as parent project
|
||||
100
frontend/angular.json
Normal file
100
frontend/angular.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization" : {
|
||||
"fonts": false
|
||||
},
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
83
frontend/package.json
Normal file
83
frontend/package.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --configuration production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "19.2.x",
|
||||
"@angular/compiler": "19.2.x",
|
||||
"@angular/core": "19.2.x",
|
||||
"@angular/forms": "19.2.x",
|
||||
"@angular/platform-browser": "19.2.x",
|
||||
"@angular/router": "19.2.x",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "19.2.x",
|
||||
"@angular/cli": "19.2.x",
|
||||
"@angular/compiler-cli": "19.2.x",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "5.x.x",
|
||||
"undici-types": "7.12.0",
|
||||
"node-releases": "2.0.21",
|
||||
"node-gyp": "11.4.2",
|
||||
"tar": "7.4.3",
|
||||
"minizlib": "3.0.2",
|
||||
"immutable": "5.1.3",
|
||||
"exponential-backoff": "3.1.2",
|
||||
"emoji-regex": "10.5.0",
|
||||
"electron-to-chromium": "1.5.221",
|
||||
"caniuse-lite": "1.0.30001743"
|
||||
},
|
||||
"resolutions": {
|
||||
"undici-types": "7.12.0",
|
||||
"node-releases": "2.0.21",
|
||||
"node-gyp": "11.4.2",
|
||||
"tar": "7.4.3",
|
||||
"minizlib": "3.0.2",
|
||||
"immutable": "5.1.3",
|
||||
"exponential-backoff": "3.1.2",
|
||||
"emoji-regex": "10.5.0",
|
||||
"electron-to-chromium": "1.5.221",
|
||||
"caniuse-lite": "1.0.30001743"
|
||||
},
|
||||
"overrides": {
|
||||
"undici-types": "7.12.0",
|
||||
"node-releases": "2.0.21",
|
||||
"node-gyp": "11.4.2",
|
||||
"tar": "7.4.3",
|
||||
"minizlib": "3.0.2",
|
||||
"immutable": "5.1.3",
|
||||
"exponential-backoff": "3.1.2",
|
||||
"emoji-regex": "10.5.0",
|
||||
"electron-to-chromium": "1.5.221",
|
||||
"caniuse-lite": "1.0.30001743"
|
||||
}
|
||||
}
|
||||
7
frontend/proxy.conf.json
Normal file
7
frontend/proxy.conf.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
frontend/src/app/app.config.ts
Normal file
13
frontend/src/app/app.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient()
|
||||
]
|
||||
};
|
||||
134
frontend/src/app/app.css
Normal file
134
frontend/src/app/app.css
Normal file
@@ -0,0 +1,134 @@
|
||||
/* App Layout */
|
||||
.app-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 280px;
|
||||
transition: margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.main-content.sidebar-collapsed {
|
||||
margin-right: 80px;
|
||||
}
|
||||
|
||||
/* Top Header */
|
||||
.top-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px;
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.top-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #3b82f6;
|
||||
font-weight: 700;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 6px 14px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.main-content {
|
||||
margin-right: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.top-header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth Scrolling */
|
||||
.content-area::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-thumb {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content-area::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Global Material Icons Sizing */
|
||||
.material-icons.md-16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.material-icons.md-18 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.material-icons.md-20 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.material-icons.md-24 {
|
||||
font-size: 24px;
|
||||
}
|
||||
342
frontend/src/app/app.html
Normal file
342
frontend/src/app/app.html
Normal file
@@ -0,0 +1,342 @@
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
<style>
|
||||
:host {
|
||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
||||
--french-violet: oklch(47.66% 0.246 305.88);
|
||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
||||
--hot-red: oklch(61.42% 0.238 15.34);
|
||||
--orange-red: oklch(63.32% 0.24 31.68);
|
||||
|
||||
--gray-900: oklch(19.37% 0.006 300.98);
|
||||
--gray-700: oklch(36.98% 0.014 302.71);
|
||||
--gray-400: oklch(70.9% 0.015 304.04);
|
||||
|
||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
||||
180deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
||||
90deg,
|
||||
var(--orange-red) 0%,
|
||||
var(--vivid-pink) 50%,
|
||||
var(--electric-violet) 100%
|
||||
);
|
||||
|
||||
--pill-accent: var(--bright-blue);
|
||||
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.125rem;
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.125rem;
|
||||
margin: 0;
|
||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
box-sizing: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.angular-logo {
|
||||
max-width: 9.2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.pill-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--pill-accent: var(--bright-blue);
|
||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
||||
color: var(--pill-accent);
|
||||
padding-inline: 0.75rem;
|
||||
padding-block: 0.375rem;
|
||||
border-radius: 2.75rem;
|
||||
border: 0;
|
||||
transition: background 0.3s ease;
|
||||
font-family: var(--inter-font);
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 1.4rem;
|
||||
letter-spacing: -0.00875rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill:hover {
|
||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 1) {
|
||||
--pill-accent: var(--bright-blue);
|
||||
}
|
||||
.pill-group .pill:nth-child(6n + 2) {
|
||||
--pill-accent: var(--electric-violet);
|
||||
}
|
||||
.pill-group .pill:nth-child(6n + 3) {
|
||||
--pill-accent: var(--french-violet);
|
||||
}
|
||||
|
||||
.pill-group .pill:nth-child(6n + 4),
|
||||
.pill-group .pill:nth-child(6n + 5),
|
||||
.pill-group .pill:nth-child(6n + 6) {
|
||||
--pill-accent: var(--hot-red);
|
||||
}
|
||||
|
||||
.pill-group svg {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.73rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.social-links path {
|
||||
transition: fill 0.3s ease;
|
||||
fill: var(--gray-400);
|
||||
}
|
||||
|
||||
.social-links a:hover svg path {
|
||||
fill: var(--gray-900);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
margin-block: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<div class="left-side">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 982 239"
|
||||
fill="none"
|
||||
class="angular-logo"
|
||||
>
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#c)"
|
||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="c"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF41F8" />
|
||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
|
||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="0"
|
||||
x2="982"
|
||||
y1="192"
|
||||
y2="192"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#F0060B" />
|
||||
<stop offset="0" stop-color="#F0070C" />
|
||||
<stop offset=".526" stop-color="#CC26D5" />
|
||||
<stop offset="1" stop-color="#7702FF" />
|
||||
</linearGradient>
|
||||
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<h1>Hello, {{ title() }}</h1>
|
||||
<p>Congratulations! Your app is running. 🎉</p>
|
||||
</div>
|
||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
||||
<div class="right-side">
|
||||
<div class="pill-group">
|
||||
@for (item of [
|
||||
{ title: 'Explore the Docs', link: 'https://angular.dev' },
|
||||
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
|
||||
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
|
||||
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
|
||||
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
|
||||
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
|
||||
]; track item.title) {
|
||||
<a
|
||||
class="pill"
|
||||
[href]="item.link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="14"
|
||||
viewBox="0 -960 960 960"
|
||||
width="14"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="social-links">
|
||||
<a
|
||||
href="https://github.com/angular/angular"
|
||||
aria-label="Github"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Github"
|
||||
>
|
||||
<path
|
||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/angular"
|
||||
aria-label="Twitter"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Twitter"
|
||||
>
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
||||
aria-label="Youtube"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg
|
||||
width="29"
|
||||
height="20"
|
||||
viewBox="0 0 29 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="Youtube"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
||||
|
||||
|
||||
<router-outlet />
|
||||
15
frontend/src/app/app.routes.ts
Normal file
15
frontend/src/app/app.routes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ArtifactsListComponent } from './components/artifacts-list/artifacts-list';
|
||||
import { UploadFormComponent } from './components/upload-form/upload-form';
|
||||
import { QueryFormComponent } from './components/query-form/query-form';
|
||||
import { SettingsComponent } from './components/settings/settings';
|
||||
import { ProfileComponent } from './components/profile/profile';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: '/artifacts', pathMatch: 'full' },
|
||||
{ path: 'artifacts', component: ArtifactsListComponent },
|
||||
{ path: 'upload', component: UploadFormComponent },
|
||||
{ path: 'query', component: QueryFormComponent },
|
||||
{ path: 'settings', component: SettingsComponent },
|
||||
{ path: 'profile', component: ProfileComponent }
|
||||
];
|
||||
23
frontend/src/app/app.spec.ts
Normal file
23
frontend/src/app/app.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
|
||||
});
|
||||
});
|
||||
52
frontend/src/app/app.ts
Normal file
52
frontend/src/app/app.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ArtifactService } from './services/artifact';
|
||||
import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, NavSidebarComponent],
|
||||
template: `
|
||||
<div class="app-layout">
|
||||
<app-nav-sidebar (sidebarToggled)="onSidebarToggle($event)"></app-nav-sidebar>
|
||||
|
||||
<main class="main-content" [class.sidebar-collapsed]="isSidebarCollapsed">
|
||||
<header class="top-header">
|
||||
<h1><span class="logo">[W13]</span> Warehouse13</h1>
|
||||
<div class="header-info">
|
||||
<span class="badge">{{ deploymentMode }}</span>
|
||||
<span class="badge">{{ storageBackend }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-area">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./app.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
deploymentMode: string = '';
|
||||
storageBackend: string = '';
|
||||
isSidebarCollapsed: boolean = false;
|
||||
|
||||
constructor(private artifactService: ArtifactService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.artifactService.getApiInfo().subscribe({
|
||||
next: (info) => {
|
||||
this.deploymentMode = `Mode: ${info.deployment_mode}`;
|
||||
this.storageBackend = `Storage: ${info.storage_backend}`;
|
||||
},
|
||||
error: (err) => console.error('Failed to load API info:', err)
|
||||
});
|
||||
}
|
||||
|
||||
onSidebarToggle(isCollapsed: boolean) {
|
||||
this.isSidebarCollapsed = isCollapsed;
|
||||
}
|
||||
}
|
||||
190
frontend/src/app/components/artifacts-list/artifacts-list.html
Normal file
190
frontend/src/app/components/artifacts-list/artifacts-list.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<div class="artifacts-container">
|
||||
<div class="toolbar">
|
||||
<button (click)="loadArtifacts()" class="btn btn-primary">
|
||||
<span class="material-icons md-16">refresh</span> Refresh
|
||||
</button>
|
||||
<button (click)="toggleAutoRefresh()"
|
||||
[class.btn-success]="autoRefreshEnabled"
|
||||
[class.btn-secondary]="!autoRefreshEnabled"
|
||||
class="btn">
|
||||
Auto-refresh: {{ autoRefreshEnabled ? 'ON' : 'OFF' }}
|
||||
</button>
|
||||
<button (click)="generateSeedData()" class="btn btn-secondary">
|
||||
<span class="material-icons md-16">auto_awesome</span> Generate Seed Data
|
||||
</button>
|
||||
|
||||
<span class="count-badge">{{ filteredArtifacts.length }} artifacts</span>
|
||||
|
||||
<div class="filter-inline">
|
||||
<span class="material-icons md-16 search-icon">search</span>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="onSearch()"
|
||||
placeholder="Search..."
|
||||
class="search-input">
|
||||
<button (click)="clearSearch()" class="btn-clear" *ngIf="searchTerm">
|
||||
<span class="material-icons md-16">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-message" *ngIf="error">{{ error }}</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="artifacts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" (click)="sortTable('sim_source_id')">
|
||||
Sim Source
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'sim_source_id'">
|
||||
{{ sortDirection === 'asc' ? '↑' : '↓' }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="sortable" (click)="sortTable('filename')">
|
||||
Artifacts
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'filename'">
|
||||
{{ sortDirection === 'asc' ? '↑' : '↓' }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="sortable" (click)="sortTable('created_at')">
|
||||
Date
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'created_at'">
|
||||
{{ sortDirection === 'asc' ? '↑' : '↓' }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="sortable" (click)="sortTable('test_name')">
|
||||
Uploaded By
|
||||
<span class="sort-indicator" *ngIf="sortColumn === 'test_name'">
|
||||
{{ sortDirection === 'asc' ? '↑' : '↓' }}
|
||||
</span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="loading">
|
||||
<td colspan="5" class="loading">Loading artifacts...</td>
|
||||
</tr>
|
||||
<tr *ngIf="!loading && filteredArtifacts.length === 0">
|
||||
<td colspan="5" class="loading">No artifacts found. Upload some files to get started!</td>
|
||||
</tr>
|
||||
<tr *ngFor="let artifact of filteredArtifacts" (click)="showDetail(artifact)" class="clickable">
|
||||
<td>{{ artifact.sim_source_id || artifact.test_suite || '-' }}</td>
|
||||
<td>
|
||||
<a href="javascript:void(0)" class="artifact-link">{{ artifact.filename }}</a>
|
||||
<div class="tags" *ngIf="artifact.tags && artifact.tags.length > 0">
|
||||
<span class="tag" *ngFor="let tag of artifact.tags">{{ tag }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ formatDate(artifact.created_at) }}</td>
|
||||
<td>{{ artifact.test_name || '-' }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button (click)="downloadArtifact(artifact, $event)" class="icon-btn" title="Download">
|
||||
<span class="material-icons md-16">download</span>
|
||||
</button>
|
||||
<button (click)="deleteArtifact(artifact, $event)" class="icon-btn danger" title="Delete">
|
||||
<span class="material-icons md-16">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button (click)="previousPage()" [disabled]="currentPage === 1" class="btn">← Previous</button>
|
||||
<span class="page-info">Page {{ currentPage }}</span>
|
||||
<button (click)="nextPage()" [disabled]="filteredArtifacts.length < pageSize" class="btn">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
<div class="modal" *ngIf="selectedArtifact" (click)="closeDetail()">
|
||||
<div class="modal-content" (click)="$event.stopPropagation()">
|
||||
<span class="close" (click)="closeDetail()">×</span>
|
||||
<h2>Artifact Details</h2>
|
||||
<div class="detail-content">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">ID</div>
|
||||
<div class="detail-value">{{ selectedArtifact.id }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Filename</div>
|
||||
<div class="detail-value">{{ selectedArtifact.filename }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">File Type</div>
|
||||
<div class="detail-value"><span class="file-type-badge">{{ selectedArtifact.file_type }}</span></div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Size</div>
|
||||
<div class="detail-value">{{ formatBytes(selectedArtifact.file_size) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Storage Path</div>
|
||||
<div class="detail-value"><code>{{ selectedArtifact.storage_path }}</code></div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Uploaded By</div>
|
||||
<div class="detail-value">{{ selectedArtifact.test_name || '-' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Sim Source</div>
|
||||
<div class="detail-value">{{ selectedArtifact.test_suite || '-' }}</div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.sim_source_id">
|
||||
<div class="detail-label">SIM Source ID</div>
|
||||
<div class="detail-value">{{ selectedArtifact.sim_source_id }}</div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.test_result">
|
||||
<div class="detail-label">Test Result</div>
|
||||
<div class="detail-value">
|
||||
<span class="result-badge result-{{ selectedArtifact.test_result }}">
|
||||
{{ selectedArtifact.test_result }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.test_config">
|
||||
<div class="detail-label">Test Config</div>
|
||||
<div class="detail-value"><pre>{{ selectedArtifact.test_config | json }}</pre></div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.custom_metadata">
|
||||
<div class="detail-label">Custom Metadata</div>
|
||||
<div class="detail-value"><pre>{{ selectedArtifact.custom_metadata | json }}</pre></div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.description">
|
||||
<div class="detail-label">Description</div>
|
||||
<div class="detail-value">{{ selectedArtifact.description }}</div>
|
||||
</div>
|
||||
<div class="detail-row" *ngIf="selectedArtifact.tags && selectedArtifact.tags.length > 0">
|
||||
<div class="detail-label">Tags</div>
|
||||
<div class="detail-value">
|
||||
<span class="tag" *ngFor="let tag of selectedArtifact.tags">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Version</div>
|
||||
<div class="detail-value">{{ selectedArtifact.version || '-' }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Created</div>
|
||||
<div class="detail-value">{{ formatDate(selectedArtifact.created_at) }}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Updated</div>
|
||||
<div class="detail-value">{{ formatDate(selectedArtifact.updated_at) }}</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button (click)="downloadArtifact(selectedArtifact, $event)" class="btn btn-primary">
|
||||
<span class="material-icons md-16">download</span> Download
|
||||
</button>
|
||||
<button (click)="deleteArtifact(selectedArtifact, $event); closeDetail()" class="btn btn-danger">
|
||||
<span class="material-icons md-16">delete</span> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ArtifactsList } from './artifacts-list';
|
||||
|
||||
describe('ArtifactsList', () => {
|
||||
let component: ArtifactsList;
|
||||
let fixture: ComponentFixture<ArtifactsList>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ArtifactsList]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ArtifactsList);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
235
frontend/src/app/components/artifacts-list/artifacts-list.ts
Normal file
235
frontend/src/app/components/artifacts-list/artifacts-list.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ArtifactService } from '../../services/artifact';
|
||||
import { Artifact } from '../../models/artifact.model';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-artifacts-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './artifacts-list.html',
|
||||
styleUrls: ['./artifacts-list.css']
|
||||
})
|
||||
export class ArtifactsListComponent implements OnInit, OnDestroy {
|
||||
artifacts: Artifact[] = [];
|
||||
filteredArtifacts: Artifact[] = [];
|
||||
selectedArtifact: Artifact | null = null;
|
||||
searchTerm: string = '';
|
||||
|
||||
// Pagination
|
||||
currentPage: number = 1;
|
||||
pageSize: number = 25;
|
||||
|
||||
// Auto-refresh
|
||||
autoRefreshEnabled: boolean = true;
|
||||
private refreshSubscription?: Subscription;
|
||||
private readonly REFRESH_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
// Sorting
|
||||
sortColumn: string | null = null;
|
||||
sortDirection: 'asc' | 'desc' = 'asc';
|
||||
|
||||
loading: boolean = false;
|
||||
error: string | null = null;
|
||||
|
||||
constructor(private artifactService: ArtifactService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadArtifacts();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
|
||||
loadArtifacts() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
const offset = (this.currentPage - 1) * this.pageSize;
|
||||
this.artifactService.listArtifacts(this.pageSize, offset).subscribe({
|
||||
next: (artifacts) => {
|
||||
this.artifacts = artifacts;
|
||||
this.applyFilter();
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error = 'Failed to load artifacts: ' + err.message;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
if (!this.searchTerm) {
|
||||
this.filteredArtifacts = [...this.artifacts];
|
||||
} else {
|
||||
const term = this.searchTerm.toLowerCase();
|
||||
this.filteredArtifacts = this.artifacts.filter(artifact =>
|
||||
artifact.filename.toLowerCase().includes(term) ||
|
||||
(artifact.test_name && artifact.test_name.toLowerCase().includes(term)) ||
|
||||
(artifact.test_suite && artifact.test_suite.toLowerCase().includes(term)) ||
|
||||
(artifact.sim_source_id && artifact.sim_source_id.toLowerCase().includes(term))
|
||||
);
|
||||
}
|
||||
|
||||
if (this.sortColumn) {
|
||||
this.applySorting();
|
||||
}
|
||||
}
|
||||
|
||||
onSearch() {
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
this.searchTerm = '';
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
sortTable(column: string) {
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
this.applySorting();
|
||||
}
|
||||
|
||||
applySorting() {
|
||||
if (!this.sortColumn) return;
|
||||
|
||||
this.filteredArtifacts.sort((a, b) => {
|
||||
const aVal = (a as any)[this.sortColumn!] || '';
|
||||
const bVal = (b as any)[this.sortColumn!] || '';
|
||||
|
||||
let comparison = 0;
|
||||
if (this.sortColumn === 'created_at') {
|
||||
comparison = new Date(aVal).getTime() - new Date(bVal).getTime();
|
||||
} else {
|
||||
comparison = String(aVal).localeCompare(String(bVal));
|
||||
}
|
||||
|
||||
return this.sortDirection === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadArtifacts();
|
||||
}
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
this.currentPage++;
|
||||
this.loadArtifacts();
|
||||
}
|
||||
|
||||
toggleAutoRefresh() {
|
||||
this.autoRefreshEnabled = !this.autoRefreshEnabled;
|
||||
if (this.autoRefreshEnabled) {
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
private startAutoRefresh() {
|
||||
if (!this.autoRefreshEnabled) return;
|
||||
|
||||
this.refreshSubscription = interval(this.REFRESH_INTERVAL)
|
||||
.pipe(switchMap(() => this.artifactService.listArtifacts(this.pageSize, (this.currentPage - 1) * this.pageSize)))
|
||||
.subscribe({
|
||||
next: (artifacts) => {
|
||||
this.artifacts = artifacts;
|
||||
this.applyFilter();
|
||||
},
|
||||
error: (err) => console.error('Auto-refresh error:', err)
|
||||
});
|
||||
}
|
||||
|
||||
private stopAutoRefresh() {
|
||||
if (this.refreshSubscription) {
|
||||
this.refreshSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
showDetail(artifact: Artifact) {
|
||||
this.selectedArtifact = artifact;
|
||||
}
|
||||
|
||||
closeDetail() {
|
||||
this.selectedArtifact = null;
|
||||
}
|
||||
|
||||
downloadArtifact(artifact: Artifact, event: Event) {
|
||||
event.stopPropagation();
|
||||
this.artifactService.downloadArtifact(artifact.id).subscribe({
|
||||
next: (blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = artifact.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
error: (err) => alert('Download failed: ' + err.message)
|
||||
});
|
||||
}
|
||||
|
||||
deleteArtifact(artifact: Artifact, event: Event) {
|
||||
event.stopPropagation();
|
||||
if (!confirm(`Are you sure you want to delete ${artifact.filename}? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.artifactService.deleteArtifact(artifact.id).subscribe({
|
||||
next: () => {
|
||||
this.loadArtifacts();
|
||||
if (this.selectedArtifact?.id === artifact.id) {
|
||||
this.closeDetail();
|
||||
}
|
||||
},
|
||||
error: (err) => alert('Delete failed: ' + err.message)
|
||||
});
|
||||
}
|
||||
|
||||
generateSeedData() {
|
||||
const count = prompt('How many artifacts to generate? (1-100)', '10');
|
||||
if (!count) return;
|
||||
|
||||
const num = parseInt(count);
|
||||
if (isNaN(num) || num < 1 || num > 100) {
|
||||
alert('Please enter a number between 1 and 100');
|
||||
return;
|
||||
}
|
||||
|
||||
this.artifactService.generateSeedData(num).subscribe({
|
||||
next: (result) => {
|
||||
alert(result.message);
|
||||
this.loadArtifacts();
|
||||
},
|
||||
error: (err) => alert('Generation failed: ' + err.message)
|
||||
});
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString();
|
||||
}
|
||||
}
|
||||
276
frontend/src/app/components/nav-sidebar/nav-sidebar.css
Normal file
276
frontend/src/app/components/nav-sidebar/nav-sidebar.css
Normal file
@@ -0,0 +1,276 @@
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* Toggle Button */
|
||||
.toggle-btn {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 20px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #3b82f6;
|
||||
border: 2px solid #0f172a;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.5);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1001;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
|
||||
.toggle-btn .material-icons {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* User Section */
|
||||
.user-section {
|
||||
padding: 24px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .user-section {
|
||||
padding: 24px 8px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .user-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 11px;
|
||||
color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Navigation Items */
|
||||
.nav-items {
|
||||
flex: 1;
|
||||
padding: 16px 8px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nav-items::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.nav-items::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.nav-items::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-item {
|
||||
justify-content: center;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.app-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.sidebar:not(.collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 60px;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
left: auto;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-label,
|
||||
.user-info {
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
40
frontend/src/app/components/nav-sidebar/nav-sidebar.html
Normal file
40
frontend/src/app/components/nav-sidebar/nav-sidebar.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<aside class="sidebar" [class.collapsed]="isCollapsed">
|
||||
<!-- Toggle Button -->
|
||||
<button class="toggle-btn" (click)="toggleSidebar()" aria-label="Toggle sidebar">
|
||||
<span class="material-icons">{{ isCollapsed ? 'chevron_right' : 'chevron_left' }}</span>
|
||||
</button>
|
||||
|
||||
<!-- User Profile Section -->
|
||||
<div class="user-section">
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text">{{ user.avatar }}</span>
|
||||
</div>
|
||||
<div class="user-info" *ngIf="!isCollapsed">
|
||||
<div class="user-name">{{ user.name }}</div>
|
||||
<div class="user-email">{{ user.email }}</div>
|
||||
<div class="user-role">{{ user.role }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Items -->
|
||||
<nav class="nav-items">
|
||||
<a
|
||||
*ngFor="let item of navItems"
|
||||
[routerLink]="item.route"
|
||||
routerLinkActive="active"
|
||||
class="nav-item"
|
||||
[attr.aria-label]="item.label"
|
||||
>
|
||||
<span class="material-icons nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- App Info Footer -->
|
||||
<div class="sidebar-footer" *ngIf="!isCollapsed">
|
||||
<div class="app-info">
|
||||
<div class="app-name">Warehouse13</div>
|
||||
<div class="app-version">v1.0.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
43
frontend/src/app/components/nav-sidebar/nav-sidebar.ts
Normal file
43
frontend/src/app/components/nav-sidebar/nav-sidebar.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
|
||||
interface NavItem {
|
||||
route: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-sidebar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, RouterLinkActive],
|
||||
templateUrl: './nav-sidebar.html',
|
||||
styleUrls: ['./nav-sidebar.css']
|
||||
})
|
||||
export class NavSidebarComponent {
|
||||
isCollapsed = false;
|
||||
|
||||
@Output() sidebarToggled = new EventEmitter<boolean>();
|
||||
|
||||
// Hardcoded user data for now (will be replaced with OAuth)
|
||||
user = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@warehouse13.com',
|
||||
avatar: 'JD',
|
||||
role: 'Administrator'
|
||||
};
|
||||
|
||||
navItems: NavItem[] = [
|
||||
{ route: '/artifacts', label: 'Artifacts', icon: 'inventory_2' },
|
||||
{ route: '/upload', label: 'Upload', icon: 'cloud_upload' },
|
||||
{ route: '/query', label: 'Query', icon: 'search' },
|
||||
{ route: '/profile', label: 'Profile', icon: 'person' },
|
||||
{ route: '/settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
toggleSidebar() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.sidebarToggled.emit(this.isCollapsed);
|
||||
}
|
||||
}
|
||||
395
frontend/src/app/components/profile/profile.css
Normal file
395
frontend/src/app/components/profile/profile.css
Normal file
@@ -0,0 +1,395 @@
|
||||
.profile-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.page-header h1 .material-icons {
|
||||
font-size: 32px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Profile Card */
|
||||
.profile-card {
|
||||
background: #1e293b;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.profile-avatar-large {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 24px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profile-info h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.email {
|
||||
color: #94a3b8;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.badge-role {
|
||||
padding: 6px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.department {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.department .material-icons {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3b82f6;
|
||||
background: transparent;
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-item > .material-icons {
|
||||
font-size: 20px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #1e293b;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: #3b82f6;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 36px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Activity Section */
|
||||
.activity-section,
|
||||
.security-section {
|
||||
background: #1e293b;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.activity-section h2,
|
||||
.security-section h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.activity-section h2 .material-icons,
|
||||
.security-section h2 .material-icons {
|
||||
font-size: 24px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-icon .material-icons {
|
||||
font-size: 20px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-action {
|
||||
font-size: 14px;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-file {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Security Section */
|
||||
.security-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.security-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.security-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.security-info > .material-icons {
|
||||
font-size: 24px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.security-info h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.security-info p {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.profile-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-avatar-large {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.security-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
128
frontend/src/app/components/profile/profile.html
Normal file
128
frontend/src/app/components/profile/profile.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<div class="profile-container">
|
||||
<header class="page-header">
|
||||
<h1>
|
||||
<span class="material-icons">person</span>
|
||||
Profile
|
||||
</h1>
|
||||
<p class="subtitle">Manage your account information</p>
|
||||
</header>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- User Info Card -->
|
||||
<section class="profile-card">
|
||||
<div class="profile-header">
|
||||
<div class="profile-avatar-large">
|
||||
<span class="avatar-text">{{ user.avatar }}</span>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<h2>{{ user.name }}</h2>
|
||||
<p class="email">{{ user.email }}</p>
|
||||
<div class="profile-meta">
|
||||
<span class="badge badge-role">{{ user.role }}</span>
|
||||
<span class="department">
|
||||
<span class="material-icons">business</span>
|
||||
{{ user.department }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-edit">
|
||||
<span class="material-icons">edit</span>
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="profile-details">
|
||||
<div class="detail-item">
|
||||
<span class="material-icons">event</span>
|
||||
<div>
|
||||
<label>Joined</label>
|
||||
<span>{{ user.joinDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="material-icons">schedule</span>
|
||||
<div>
|
||||
<label>Last Login</label>
|
||||
<span>{{ user.lastLogin }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<section class="stats-grid">
|
||||
<div class="stat-card" *ngFor="let stat of stats">
|
||||
<span class="material-icons stat-icon">{{ stat.icon }}</span>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<section class="activity-section">
|
||||
<h2>
|
||||
<span class="material-icons">history</span>
|
||||
Recent Activity
|
||||
</h2>
|
||||
<div class="activity-list">
|
||||
<div class="activity-item" *ngFor="let activity of recentActivity">
|
||||
<div class="activity-icon">
|
||||
<span class="material-icons">{{
|
||||
activity.action.includes('Uploaded') ? 'cloud_upload' :
|
||||
activity.action.includes('query') ? 'search' :
|
||||
activity.action.includes('Downloaded') ? 'cloud_download' :
|
||||
'settings'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="activity-details">
|
||||
<div class="activity-action">{{ activity.action }}</div>
|
||||
<div class="activity-file">{{ activity.file }}</div>
|
||||
</div>
|
||||
<div class="activity-time">{{ activity.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Security Section -->
|
||||
<section class="security-section">
|
||||
<h2>
|
||||
<span class="material-icons">security</span>
|
||||
Security
|
||||
</h2>
|
||||
<div class="security-content">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<span class="material-icons">lock</span>
|
||||
<div>
|
||||
<h3>Password</h3>
|
||||
<p>Last changed 3 months ago</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-secondary">Change Password</button>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<span class="material-icons">verified_user</span>
|
||||
<div>
|
||||
<h3>Two-Factor Authentication</h3>
|
||||
<p>Add an extra layer of security</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-secondary">Enable 2FA</button>
|
||||
</div>
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<span class="material-icons">devices</span>
|
||||
<div>
|
||||
<h3>Active Sessions</h3>
|
||||
<p>Manage your active login sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-secondary">View Sessions</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
36
frontend/src/app/components/profile/profile.ts
Normal file
36
frontend/src/app/components/profile/profile.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './profile.html',
|
||||
styleUrls: ['./profile.css']
|
||||
})
|
||||
export class ProfileComponent {
|
||||
// Hardcoded user data (will be replaced with OAuth integration)
|
||||
user = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@warehouse13.com',
|
||||
avatar: 'JD',
|
||||
role: 'Administrator',
|
||||
department: 'Engineering',
|
||||
joinDate: 'January 15, 2024',
|
||||
lastLogin: 'October 16, 2025, 2:30 PM'
|
||||
};
|
||||
|
||||
stats = [
|
||||
{ label: 'Artifacts Uploaded', value: '1,234', icon: 'cloud_upload' },
|
||||
{ label: 'Queries Run', value: '567', icon: 'search' },
|
||||
{ label: 'Storage Used', value: '45.2 GB', icon: 'storage' },
|
||||
{ label: 'Active Since', value: '9 months', icon: 'schedule' }
|
||||
];
|
||||
|
||||
recentActivity = [
|
||||
{ action: 'Uploaded artifact', file: 'test_results.csv', time: '2 hours ago' },
|
||||
{ action: 'Ran query', file: 'integration tests', time: '5 hours ago' },
|
||||
{ action: 'Downloaded artifact', file: 'performance_metrics.json', time: '1 day ago' },
|
||||
{ action: 'Updated settings', file: 'notification preferences', time: '2 days ago' }
|
||||
];
|
||||
}
|
||||
102
frontend/src/app/components/query-form/query-form.html
Normal file
102
frontend/src/app/components/query-form/query-form.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<div class="query-section">
|
||||
<h2>Query Artifacts</h2>
|
||||
<form [formGroup]="queryForm" (ngSubmit)="onSubmit()">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-filename">Filename</label>
|
||||
<input
|
||||
type="text"
|
||||
id="q-filename"
|
||||
formControlName="filename"
|
||||
placeholder="Search filename...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-type">File Type</label>
|
||||
<select id="q-type" formControlName="file_type">
|
||||
<option value="">All</option>
|
||||
<option value="csv">CSV</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">Binary</option>
|
||||
<option value="pcap">PCAP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-test-name">Test Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="q-test-name"
|
||||
formControlName="test_name"
|
||||
placeholder="Search test name...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-suite">Test Suite</label>
|
||||
<input
|
||||
type="text"
|
||||
id="q-suite"
|
||||
formControlName="test_suite"
|
||||
placeholder="e.g., integration">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-sim-source-id">SIM Source ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id="q-sim-source-id"
|
||||
formControlName="sim_source_id"
|
||||
placeholder="e.g., sim_run_abc123">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-result">Test Result</label>
|
||||
<select id="q-result" formControlName="test_result">
|
||||
<option value="">All</option>
|
||||
<option value="pass">Pass</option>
|
||||
<option value="fail">Fail</option>
|
||||
<option value="skip">Skip</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-tags">Tags (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="q-tags"
|
||||
formControlName="tags"
|
||||
placeholder="e.g., regression, smoke">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-start-date">Start Date</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="q-start-date"
|
||||
formControlName="start_date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-end-date">End Date</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="q-end-date"
|
||||
formControlName="end_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn btn-primary btn-large">
|
||||
<span class="material-icons md-18">search</span> Search
|
||||
</button>
|
||||
<button type="button" (click)="clearForm()" class="btn btn-secondary">
|
||||
<span class="material-icons md-18">close</span> Clear
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
23
frontend/src/app/components/query-form/query-form.spec.ts
Normal file
23
frontend/src/app/components/query-form/query-form.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { QueryForm } from './query-form';
|
||||
|
||||
describe('QueryForm', () => {
|
||||
let component: QueryForm;
|
||||
let fixture: ComponentFixture<QueryForm>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [QueryForm]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(QueryForm);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
83
frontend/src/app/components/query-form/query-form.ts
Normal file
83
frontend/src/app/components/query-form/query-form.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ArtifactService } from '../../services/artifact';
|
||||
import { Artifact, ArtifactQuery } from '../../models/artifact.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-query-form',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './query-form.html',
|
||||
styleUrls: ['./query-form.css']
|
||||
})
|
||||
export class QueryFormComponent {
|
||||
queryForm: FormGroup;
|
||||
@Output() resultsFound = new EventEmitter<Artifact[]>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private artifactService: ArtifactService
|
||||
) {
|
||||
this.queryForm = this.fb.group({
|
||||
filename: [''],
|
||||
file_type: [''],
|
||||
test_name: [''],
|
||||
test_suite: [''],
|
||||
test_result: [''],
|
||||
sim_source_id: [''],
|
||||
tags: [''],
|
||||
start_date: [''],
|
||||
end_date: ['']
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const query: ArtifactQuery = {
|
||||
limit: 100,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
if (this.queryForm.value.filename) {
|
||||
query.filename = this.queryForm.value.filename;
|
||||
}
|
||||
if (this.queryForm.value.file_type) {
|
||||
query.file_type = this.queryForm.value.file_type;
|
||||
}
|
||||
if (this.queryForm.value.test_name) {
|
||||
query.test_name = this.queryForm.value.test_name;
|
||||
}
|
||||
if (this.queryForm.value.test_suite) {
|
||||
query.test_suite = this.queryForm.value.test_suite;
|
||||
}
|
||||
if (this.queryForm.value.test_result) {
|
||||
query.test_result = this.queryForm.value.test_result;
|
||||
}
|
||||
if (this.queryForm.value.sim_source_id) {
|
||||
query.sim_source_id = this.queryForm.value.sim_source_id;
|
||||
}
|
||||
if (this.queryForm.value.tags) {
|
||||
query.tags = this.queryForm.value.tags
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter((t: string) => t);
|
||||
}
|
||||
if (this.queryForm.value.start_date) {
|
||||
query.start_date = new Date(this.queryForm.value.start_date).toISOString();
|
||||
}
|
||||
if (this.queryForm.value.end_date) {
|
||||
query.end_date = new Date(this.queryForm.value.end_date).toISOString();
|
||||
}
|
||||
|
||||
this.artifactService.queryArtifacts(query).subscribe({
|
||||
next: (artifacts) => {
|
||||
this.resultsFound.emit(artifacts);
|
||||
},
|
||||
error: (err) => alert('Query failed: ' + err.message)
|
||||
});
|
||||
}
|
||||
|
||||
clearForm() {
|
||||
this.queryForm.reset();
|
||||
}
|
||||
}
|
||||
227
frontend/src/app/components/settings/settings.css
Normal file
227
frontend/src/app/components/settings/settings.css
Normal file
@@ -0,0 +1,227 @@
|
||||
.settings-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.page-header h1 .material-icons {
|
||||
font-size: 32px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: #1e293b;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-info > .material-icons {
|
||||
font-size: 24px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.setting-info h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #f1f5f9;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.setting-info p {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #334155;
|
||||
transition: 0.3s;
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* Theme Buttons */
|
||||
.theme-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.theme-btn:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.theme-btn.active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-secondary {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
.endpoint {
|
||||
padding: 6px 12px;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.settings-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
167
frontend/src/app/components/settings/settings.html
Normal file
167
frontend/src/app/components/settings/settings.html
Normal file
@@ -0,0 +1,167 @@
|
||||
<div class="settings-container">
|
||||
<header class="page-header">
|
||||
<h1>
|
||||
<span class="material-icons">settings</span>
|
||||
Settings
|
||||
</h1>
|
||||
<p class="subtitle">Manage your Warehouse13 preferences</p>
|
||||
</header>
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- General Settings -->
|
||||
<section class="settings-section">
|
||||
<h2>General</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">palette</span>
|
||||
<div>
|
||||
<h3>Theme</h3>
|
||||
<p>Choose your preferred color scheme</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button
|
||||
[class.active]="settings.theme === 'dark'"
|
||||
(click)="changeTheme('dark')"
|
||||
class="theme-btn">
|
||||
Dark
|
||||
</button>
|
||||
<button
|
||||
[class.active]="settings.theme === 'light'"
|
||||
(click)="changeTheme('light')"
|
||||
class="theme-btn">
|
||||
Light
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">notifications</span>
|
||||
<div>
|
||||
<h3>Notifications</h3>
|
||||
<p>Enable desktop notifications for updates</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="settings.notifications"
|
||||
(change)="toggleNotifications()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">refresh</span>
|
||||
<div>
|
||||
<h3>Auto Refresh</h3>
|
||||
<p>Automatically refresh artifact list</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="settings.autoRefresh"
|
||||
(change)="toggleAutoRefresh()">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Storage Settings -->
|
||||
<section class="settings-section">
|
||||
<h2>Storage</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">storage</span>
|
||||
<div>
|
||||
<h3>Default Storage Backend</h3>
|
||||
<p>Currently using: MinIO</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<span class="badge badge-success">Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">cloud</span>
|
||||
<div>
|
||||
<h3>Upload Limit</h3>
|
||||
<p>Maximum file size: 500MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Settings -->
|
||||
<section class="settings-section">
|
||||
<h2>API</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">link</span>
|
||||
<div>
|
||||
<h3>API Endpoint</h3>
|
||||
<p>Backend service endpoint</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<code class="endpoint">/api/v1</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">key</span>
|
||||
<div>
|
||||
<h3>API Keys</h3>
|
||||
<p>Manage API authentication tokens</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button class="btn-secondary">Manage Keys</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About Section -->
|
||||
<section class="settings-section">
|
||||
<h2>About</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">info</span>
|
||||
<div>
|
||||
<h3>Version</h3>
|
||||
<p>Warehouse13 v1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="material-icons">description</span>
|
||||
<div>
|
||||
<h3>Documentation</h3>
|
||||
<p>View API documentation and guides</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<a href="/docs" target="_blank" class="btn-secondary">
|
||||
View Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
30
frontend/src/app/components/settings/settings.ts
Normal file
30
frontend/src/app/components/settings/settings.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './settings.html',
|
||||
styleUrls: ['./settings.css']
|
||||
})
|
||||
export class SettingsComponent {
|
||||
settings = {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
autoRefresh: false,
|
||||
refreshInterval: 30
|
||||
};
|
||||
|
||||
toggleNotifications() {
|
||||
this.settings.notifications = !this.settings.notifications;
|
||||
}
|
||||
|
||||
toggleAutoRefresh() {
|
||||
this.settings.autoRefresh = !this.settings.autoRefresh;
|
||||
}
|
||||
|
||||
changeTheme(theme: string) {
|
||||
this.settings.theme = theme;
|
||||
}
|
||||
}
|
||||
109
frontend/src/app/components/upload-form/upload-form.html
Normal file
109
frontend/src/app/components/upload-form/upload-form.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<div class="upload-section">
|
||||
<h2>Upload Artifact</h2>
|
||||
<form [formGroup]="uploadForm" (ngSubmit)="onSubmit()">
|
||||
<div class="form-group">
|
||||
<label for="file">File *</label>
|
||||
<input type="file" id="file" (change)="onFileSelected($event)" required>
|
||||
<small>Supported: CSV, JSON, binary files, PCAP</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sim-source">Sim Source *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sim-source"
|
||||
formControlName="sim_source"
|
||||
placeholder="e.g., Jenkins, GitLab CI"
|
||||
required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="uploaded-by">Uploaded By *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="uploaded-by"
|
||||
formControlName="uploaded_by"
|
||||
placeholder="e.g., john.doe"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sim-source-id">SIM Source ID (for grouping)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sim-source-id"
|
||||
formControlName="sim_source_id"
|
||||
placeholder="e.g., sim_run_20251015_001">
|
||||
<small>Use same ID for multiple artifacts from same source</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags (comma-separated) *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tags"
|
||||
formControlName="tags"
|
||||
placeholder="e.g., regression, smoke, critical"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="test-result">Test Result</label>
|
||||
<select id="test-result" formControlName="test_result">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="pass">Pass</option>
|
||||
<option value="fail">Fail</option>
|
||||
<option value="skip">Skip</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version">Version</label>
|
||||
<input
|
||||
type="text"
|
||||
id="version"
|
||||
formControlName="version"
|
||||
placeholder="e.g., v1.0.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
formControlName="description"
|
||||
rows="3"
|
||||
placeholder="Describe this artifact..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="test-config">Test Config (JSON)</label>
|
||||
<textarea
|
||||
id="test-config"
|
||||
formControlName="test_config"
|
||||
rows="4"
|
||||
placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="custom-metadata">Custom Metadata (JSON)</label>
|
||||
<textarea
|
||||
id="custom-metadata"
|
||||
formControlName="custom_metadata"
|
||||
rows="4"
|
||||
placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large" [disabled]="uploading">
|
||||
<span class="material-icons md-18">upload</span>
|
||||
{{ uploading ? 'Uploading...' : 'Upload Artifact' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div *ngIf="uploadStatus" class="upload-status" [class.success]="uploadStatus.success" [class.error]="!uploadStatus.success">
|
||||
{{ uploadStatus.message }}
|
||||
</div>
|
||||
</div>
|
||||
23
frontend/src/app/components/upload-form/upload-form.spec.ts
Normal file
23
frontend/src/app/components/upload-form/upload-form.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UploadForm } from './upload-form';
|
||||
|
||||
describe('UploadForm', () => {
|
||||
let component: UploadForm;
|
||||
let fixture: ComponentFixture<UploadForm>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UploadForm]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UploadForm);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
132
frontend/src/app/components/upload-form/upload-form.ts
Normal file
132
frontend/src/app/components/upload-form/upload-form.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ArtifactService } from '../../services/artifact';
|
||||
|
||||
@Component({
|
||||
selector: 'app-upload-form',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './upload-form.html',
|
||||
styleUrls: ['./upload-form.css']
|
||||
})
|
||||
export class UploadFormComponent {
|
||||
uploadForm: FormGroup;
|
||||
selectedFile: File | null = null;
|
||||
uploading: boolean = false;
|
||||
uploadStatus: { message: string, success: boolean } | null = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private artifactService: ArtifactService
|
||||
) {
|
||||
this.uploadForm = this.fb.group({
|
||||
file: [null, Validators.required],
|
||||
sim_source: ['', Validators.required],
|
||||
uploaded_by: ['', Validators.required],
|
||||
sim_source_id: [''],
|
||||
tags: ['', Validators.required],
|
||||
test_result: [''],
|
||||
version: [''],
|
||||
description: [''],
|
||||
test_config: [''],
|
||||
custom_metadata: ['']
|
||||
});
|
||||
}
|
||||
|
||||
onFileSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
this.selectedFile = input.files[0];
|
||||
this.uploadForm.patchValue({ file: this.selectedFile });
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (!this.uploadForm.valid || !this.selectedFile) {
|
||||
this.showStatus('Please fill in all required fields and select a file', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate JSON fields
|
||||
const testConfig = this.uploadForm.value.test_config;
|
||||
const customMetadata = this.uploadForm.value.custom_metadata;
|
||||
|
||||
if (testConfig) {
|
||||
try {
|
||||
JSON.parse(testConfig);
|
||||
} catch (e) {
|
||||
this.showStatus('Invalid Test Config JSON', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (customMetadata) {
|
||||
try {
|
||||
JSON.parse(customMetadata);
|
||||
} catch (e) {
|
||||
this.showStatus('Invalid Custom Metadata JSON', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
formData.append('test_suite', this.uploadForm.value.sim_source);
|
||||
formData.append('test_name', this.uploadForm.value.uploaded_by);
|
||||
|
||||
if (this.uploadForm.value.sim_source_id) {
|
||||
formData.append('sim_source_id', this.uploadForm.value.sim_source_id);
|
||||
}
|
||||
|
||||
// Parse and append tags as JSON array
|
||||
if (this.uploadForm.value.tags) {
|
||||
const tagsArray = this.uploadForm.value.tags
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter((t: string) => t);
|
||||
formData.append('tags', JSON.stringify(tagsArray));
|
||||
}
|
||||
|
||||
if (this.uploadForm.value.test_result) {
|
||||
formData.append('test_result', this.uploadForm.value.test_result);
|
||||
}
|
||||
|
||||
if (this.uploadForm.value.version) {
|
||||
formData.append('version', this.uploadForm.value.version);
|
||||
}
|
||||
|
||||
if (this.uploadForm.value.description) {
|
||||
formData.append('description', this.uploadForm.value.description);
|
||||
}
|
||||
|
||||
if (testConfig) {
|
||||
formData.append('test_config', testConfig);
|
||||
}
|
||||
|
||||
if (customMetadata) {
|
||||
formData.append('custom_metadata', customMetadata);
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.artifactService.uploadArtifact(formData).subscribe({
|
||||
next: (artifact) => {
|
||||
this.showStatus(`Successfully uploaded: ${artifact.filename}`, true);
|
||||
this.uploadForm.reset();
|
||||
this.selectedFile = null;
|
||||
this.uploading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.showStatus('Upload failed: ' + err.error?.detail || err.message, false);
|
||||
this.uploading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showStatus(message: string, success: boolean) {
|
||||
this.uploadStatus = { message, success };
|
||||
setTimeout(() => {
|
||||
this.uploadStatus = null;
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
40
frontend/src/app/models/artifact.model.ts
Normal file
40
frontend/src/app/models/artifact.model.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface Artifact {
|
||||
id: number;
|
||||
filename: string;
|
||||
file_type: string;
|
||||
file_size: number;
|
||||
storage_path: string;
|
||||
content_type: string | null;
|
||||
test_name: string | null;
|
||||
test_suite: string | null;
|
||||
test_config: any;
|
||||
test_result: string | null;
|
||||
sim_source_id: string | null;
|
||||
custom_metadata: any;
|
||||
description: string | null;
|
||||
tags: string[] | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: string | null;
|
||||
parent_id: number | null;
|
||||
}
|
||||
|
||||
export interface ArtifactQuery {
|
||||
filename?: string;
|
||||
file_type?: string;
|
||||
test_name?: string;
|
||||
test_suite?: string;
|
||||
test_result?: string;
|
||||
sim_source_id?: string;
|
||||
tags?: string[];
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface ApiInfo {
|
||||
deployment_mode: string;
|
||||
storage_backend: string;
|
||||
version: string;
|
||||
}
|
||||
51
frontend/src/app/services/artifact.ts
Normal file
51
frontend/src/app/services/artifact.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Artifact, ArtifactQuery, ApiInfo } from '../models/artifact.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ArtifactService {
|
||||
private apiUrl = '/api/v1/artifacts';
|
||||
private baseUrl = '/api';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getApiInfo(): Observable<ApiInfo> {
|
||||
return this.http.get<ApiInfo>(this.baseUrl);
|
||||
}
|
||||
|
||||
listArtifacts(limit: number = 100, offset: number = 0): Observable<Artifact[]> {
|
||||
const params = new HttpParams()
|
||||
.set('limit', limit.toString())
|
||||
.set('offset', offset.toString());
|
||||
return this.http.get<Artifact[]>(this.apiUrl + '/', { params });
|
||||
}
|
||||
|
||||
getArtifact(id: number): Observable<Artifact> {
|
||||
return this.http.get<Artifact>(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
queryArtifacts(query: ArtifactQuery): Observable<Artifact[]> {
|
||||
return this.http.post<Artifact[]>(`${this.apiUrl}/query`, query);
|
||||
}
|
||||
|
||||
uploadArtifact(formData: FormData): Observable<Artifact> {
|
||||
return this.http.post<Artifact>(`${this.apiUrl}/upload`, formData);
|
||||
}
|
||||
|
||||
downloadArtifact(id: number): Observable<Blob> {
|
||||
return this.http.get(`${this.apiUrl}/${id}/download`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
deleteArtifact(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.apiUrl}/${id}`);
|
||||
}
|
||||
|
||||
generateSeedData(count: number): Observable<any> {
|
||||
return this.http.post(`/api/v1/seed/generate/${count}`, {});
|
||||
}
|
||||
}
|
||||
14
frontend/src/index.html
Normal file
14
frontend/src/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Frontend</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
frontend/src/main.ts
Normal file
6
frontend/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
@@ -33,6 +33,21 @@ header {
|
||||
header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
color: #60a5fa;
|
||||
letter-spacing: -1px;
|
||||
padding: 2px 4px;
|
||||
border: 2px solid #60a5fa;
|
||||
border-radius: 4px;
|
||||
background: rgba(96, 165, 250, 0.1);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
@@ -524,6 +539,49 @@ code {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* Artifact link styles - softer blue */
|
||||
.artifact-link {
|
||||
color: #93c5fd;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.artifact-link:hover {
|
||||
color: #bfdbfe;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Clickable row cursor */
|
||||
tr.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Search icon color */
|
||||
.search-icon {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Material Icons */
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.material-icons.md-16 { font-size: 16px; }
|
||||
.material-icons.md-18 { font-size: 18px; }
|
||||
.material-icons.md-20 { font-size: 20px; }
|
||||
.material-icons.md-24 { font-size: 24px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
15
frontend/tsconfig.app.json
Normal file
15
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
34
frontend/tsconfig.json
Normal file
34
frontend/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
frontend/tsconfig.spec.json
Normal file
14
frontend/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: datalake
|
||||
description: Test Artifact Data Lake - Store and query test artifacts
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- testing
|
||||
- artifacts
|
||||
- storage
|
||||
- data-lake
|
||||
maintainers:
|
||||
- name: Your Team
|
||||
42
helm/README.md
Normal file
42
helm/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Helm Charts
|
||||
|
||||
This directory contains Helm charts for deploying Warehouse13.
|
||||
|
||||
## Current Chart (Recommended)
|
||||
|
||||
**Location:** `./warehouse13/`
|
||||
|
||||
The latest, fully-featured Helm chart with:
|
||||
- Warehouse13 branding
|
||||
- Configurable images for all components
|
||||
- Multiple deployment scenarios (dev, production, air-gapped)
|
||||
- Comprehensive documentation
|
||||
- Example values files
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
helm install warehouse13 ./warehouse13
|
||||
```
|
||||
|
||||
**Documentation:** See [warehouse13/README.md](./warehouse13/README.md)
|
||||
|
||||
## Migration from Legacy Chart
|
||||
|
||||
If you were using an older version of the chart, migration is straightforward:
|
||||
|
||||
```bash
|
||||
# Uninstall old chart (if named "datalake" or other name)
|
||||
helm uninstall <old-release-name>
|
||||
|
||||
# Install new chart
|
||||
helm install warehouse13 ./warehouse13 --namespace warehouse13 --create-namespace
|
||||
|
||||
# Or upgrade in place (if compatible)
|
||||
helm upgrade <old-release-name> ./warehouse13
|
||||
```
|
||||
|
||||
Note: Check your values.yaml configuration and update image repositories, resource limits, and other settings as needed.
|
||||
|
||||
## Quick Start
|
||||
|
||||
See [../docs/HELM-DEPLOYMENT.md](../docs/HELM-DEPLOYMENT.md) for comprehensive deployment guide.
|
||||
@@ -1,111 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "datalake.fullname" . }}
|
||||
labels:
|
||||
{{- include "datalake.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "datalake.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "datalake.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "datalake.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
key: database-url
|
||||
- name: STORAGE_BACKEND
|
||||
value: {{ .Values.config.storageBackend | quote }}
|
||||
- name: MAX_UPLOAD_SIZE
|
||||
value: {{ .Values.config.maxUploadSize | quote }}
|
||||
{{- if eq .Values.config.storageBackend "s3" }}
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
key: aws-access-key-id
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
key: aws-secret-access-key
|
||||
- name: AWS_REGION
|
||||
value: {{ .Values.aws.region | quote }}
|
||||
- name: S3_BUCKET_NAME
|
||||
value: {{ .Values.aws.bucketName | quote }}
|
||||
{{- else }}
|
||||
- name: MINIO_ENDPOINT
|
||||
value: "{{ include "datalake.fullname" . }}-minio:9000"
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
key: minio-access-key
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
key: minio-secret-key
|
||||
- name: MINIO_BUCKET_NAME
|
||||
value: "test-artifacts"
|
||||
- name: MINIO_SECURE
|
||||
value: "false"
|
||||
{{- end }}
|
||||
{{- with .Values.env }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "datalake.fullname" . }}-secrets
|
||||
labels:
|
||||
{{- include "datalake.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
database-url: "postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ include "datalake.fullname" . }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
|
||||
{{- if .Values.aws.enabled }}
|
||||
aws-access-key-id: {{ .Values.aws.accessKeyId | quote }}
|
||||
aws-secret-access-key: {{ .Values.aws.secretAccessKey | quote }}
|
||||
{{- else }}
|
||||
minio-access-key: {{ .Values.minio.rootUser | quote }}
|
||||
minio-secret-key: {{ .Values.minio.rootPassword | quote }}
|
||||
{{- end }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "datalake.fullname" . }}
|
||||
labels:
|
||||
{{- include "datalake.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "datalake.selectorLabels" . | nindent 4 }}
|
||||
111
helm/values.yaml
111
helm/values.yaml
@@ -1,111 +0,0 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: datalake
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: datalake.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
# Application configuration
|
||||
config:
|
||||
storageBackend: minio # or "s3"
|
||||
maxUploadSize: 524288000 # 500MB
|
||||
|
||||
# PostgreSQL configuration
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
username: user
|
||||
password: password
|
||||
database: datalake
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
|
||||
# MinIO configuration (for self-hosted storage)
|
||||
minio:
|
||||
enabled: true
|
||||
mode: standalone
|
||||
rootUser: minioadmin
|
||||
rootPassword: minioadmin
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 50Gi
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 9000
|
||||
consoleService:
|
||||
port: 9001
|
||||
|
||||
# AWS S3 configuration (when using AWS)
|
||||
aws:
|
||||
enabled: false
|
||||
accessKeyId: ""
|
||||
secretAccessKey: ""
|
||||
region: us-east-1
|
||||
bucketName: test-artifacts
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
- name: API_HOST
|
||||
value: "0.0.0.0"
|
||||
- name: API_PORT
|
||||
value: "8000"
|
||||
23
helm/warehouse13/.helmignore
Normal file
23
helm/warehouse13/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
281
helm/warehouse13/ARCHITECTURE.md
Normal file
281
helm/warehouse13/ARCHITECTURE.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Warehouse13 Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Warehouse13 uses a **unified application container** that includes both the frontend and backend in a single Docker image using a multi-stage build.
|
||||
|
||||
## Docker Build Strategy
|
||||
|
||||
### Multi-Stage Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build Angular Frontend
|
||||
FROM node:24-alpine AS frontend-build
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY frontend/ ./
|
||||
RUN npm run build:prod
|
||||
|
||||
# Stage 2: Python Backend with Static Frontend
|
||||
FROM python:3.11-alpine
|
||||
WORKDIR /app
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
# Copy backend code
|
||||
COPY app/ ./app/
|
||||
# Copy built frontend from stage 1
|
||||
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
|
||||
# Run FastAPI server
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Simplified Deployment** - Single container to manage
|
||||
2. **Reduced Resource Usage** - No separate nginx container needed
|
||||
3. **Easier Scaling** - Scale one deployment instead of two
|
||||
4. **Consistent Versioning** - Frontend and backend versions always match
|
||||
5. **Faster Deployments** - Fewer containers to orchestrate
|
||||
|
||||
## Service Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ warehouse13-app │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ FastAPI Backend (Port 8000) │ │
|
||||
│ │ ├── /api/* → REST API │ │
|
||||
│ │ ├── /health → Health check │ │
|
||||
│ │ ├── /docs → API documentation │ │
|
||||
│ │ └── /* → Angular SPA │ │
|
||||
│ │ │ │
|
||||
│ │ Static Files: /static/ │ │
|
||||
│ │ └── Angular build output │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
├────────────────┐
|
||||
↓ ↓
|
||||
┌──────────┐ ┌────────────┐
|
||||
│PostgreSQL│ │ MinIO │
|
||||
│(Metadata)│ │ (Blobs) │
|
||||
└──────────┘ └────────────┘
|
||||
```
|
||||
|
||||
## Helm Chart Structure
|
||||
|
||||
### Single Application Deployment
|
||||
|
||||
The Helm chart creates:
|
||||
|
||||
1. **1 Deployment**: `warehouse13-app`
|
||||
- Runs the unified container
|
||||
- Configurable replicas (default: 2)
|
||||
- Health checks on `/health` endpoint
|
||||
|
||||
2. **1 Service**: `warehouse13-app`
|
||||
- Exposes port 8000
|
||||
- Routes all traffic to the application
|
||||
|
||||
3. **Optional Ingress**
|
||||
- All paths route to `warehouse13-app:8000`
|
||||
- FastAPI handles routing internally
|
||||
|
||||
### Kubernetes Resources
|
||||
|
||||
```yaml
|
||||
# warehouse13-app Deployment
|
||||
- Replicas: 2 (configurable)
|
||||
- Port: 8000
|
||||
- Health checks: /health
|
||||
- Environment: DATABASE_URL, MINIO_* vars
|
||||
|
||||
# warehouse13-app Service
|
||||
- Type: ClusterIP
|
||||
- Port: 8000 → 8000
|
||||
|
||||
# Ingress (optional)
|
||||
- Path: / → warehouse13-app:8000
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Image Configuration
|
||||
|
||||
In `values.yaml`:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
enabled: true
|
||||
image:
|
||||
repository: warehouse13/app # Single unified image
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 2
|
||||
resources:
|
||||
requests:
|
||||
memory: "384Mi" # Combined frontend + backend
|
||||
cpu: "350m"
|
||||
limits:
|
||||
memory: "768Mi"
|
||||
cpu: "750m"
|
||||
```
|
||||
|
||||
### Accessing the Application
|
||||
|
||||
**Via Port Forward:**
|
||||
```bash
|
||||
kubectl port-forward svc/warehouse13-app 8000:8000
|
||||
```
|
||||
|
||||
Then access:
|
||||
- Frontend: http://localhost:8000
|
||||
- API: http://localhost:8000/api
|
||||
- API Docs: http://localhost:8000/docs
|
||||
- Health: http://localhost:8000/health
|
||||
|
||||
**Via Ingress:**
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: warehouse13.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: app # All traffic to one service
|
||||
```
|
||||
|
||||
## Migration from Separate Services
|
||||
|
||||
If you previously had separate `api` and `frontend` deployments:
|
||||
|
||||
### Before (Old Architecture)
|
||||
```yaml
|
||||
# values.yaml (old)
|
||||
api:
|
||||
image: warehouse13/api
|
||||
replicas: 2
|
||||
|
||||
frontend:
|
||||
image: warehouse13/frontend
|
||||
replicas: 2
|
||||
|
||||
# Two deployments, two services
|
||||
```
|
||||
|
||||
### After (Current Architecture)
|
||||
```yaml
|
||||
# values.yaml (current)
|
||||
app:
|
||||
image: warehouse13/app # Unified image
|
||||
replicas: 2
|
||||
|
||||
# One deployment, one service
|
||||
```
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. **Update values.yaml** - Change from `api`/`frontend` to `app`
|
||||
2. **Update image references** - Use `warehouse13/app` instead of separate images
|
||||
3. **Update ingress** - Point all paths to `app` backend
|
||||
4. **Deploy** - Helm will handle the transition
|
||||
5. **Verify** - Check that both frontend and API work through single service
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Building the Image
|
||||
|
||||
```bash
|
||||
# Build unified image
|
||||
docker build -t warehouse13/app:dev .
|
||||
|
||||
# Or for air-gapped environments with custom registry
|
||||
docker build \
|
||||
--build-arg NPM_REGISTRY=https://registry.npmjs.org/ \
|
||||
-t warehouse13/app:v1.0.0 .
|
||||
```
|
||||
|
||||
### Testing Locally
|
||||
|
||||
```bash
|
||||
docker run -p 8000:8000 \
|
||||
-e DATABASE_URL=postgresql://user:pass@host/db \
|
||||
-e MINIO_ENDPOINT=minio:9000 \
|
||||
warehouse13/app:dev
|
||||
```
|
||||
|
||||
Access:
|
||||
- Frontend: http://localhost:8000
|
||||
- API: http://localhost:8000/docs
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Resource Allocation
|
||||
|
||||
The unified container combines both frontend serving and API processing:
|
||||
|
||||
- **Memory**: Angular assets (~50MB) + Python runtime (~100MB) + working memory
|
||||
- **CPU**: Primarily used for API requests; static file serving is lightweight
|
||||
- **Recommended Minimum**: 384Mi memory, 350m CPU
|
||||
- **Production**: 768Mi memory, 750m CPU per replica
|
||||
|
||||
### Scaling Strategy
|
||||
|
||||
Scale horizontally by increasing replicas:
|
||||
|
||||
```bash
|
||||
# Scale to 5 replicas
|
||||
kubectl scale deployment warehouse13-app --replicas=5
|
||||
|
||||
# Or via Helm
|
||||
helm upgrade warehouse13 ./helm/warehouse13 --set app.replicas=5
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
FastAPI serves static files efficiently with:
|
||||
- ETag support
|
||||
- Browser caching headers
|
||||
- Gzip compression (if enabled in FastAPI config)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Frontend Not Loading
|
||||
|
||||
```bash
|
||||
# Check if static files exist in container
|
||||
kubectl exec -it warehouse13-app-xxx -- ls -la /app/static/
|
||||
|
||||
# Should see: index.html, *.js, *.css files
|
||||
```
|
||||
|
||||
### API Not Working
|
||||
|
||||
```bash
|
||||
# Check API health
|
||||
kubectl exec -it warehouse13-app-xxx -- curl http://localhost:8000/health
|
||||
|
||||
# Check logs
|
||||
kubectl logs warehouse13-app-xxx -f
|
||||
```
|
||||
|
||||
### Both Frontend and API Issues
|
||||
|
||||
```bash
|
||||
# Check if app is running
|
||||
kubectl get pods -l app.kubernetes.io/component=app
|
||||
|
||||
# Check service
|
||||
kubectl get svc warehouse13-app
|
||||
|
||||
# Test connectivity
|
||||
kubectl port-forward svc/warehouse13-app 8000:8000
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The unified architecture simplifies deployment and operations while maintaining the same functionality. All routing, caching, and API requests are handled by a single FastAPI application that serves both the Angular SPA and the REST API endpoints.
|
||||
16
helm/warehouse13/Chart.yaml
Normal file
16
helm/warehouse13/Chart.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v2
|
||||
name: warehouse13
|
||||
description: Warehouse13 - Enterprise Test Artifact Storage
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- testing
|
||||
- artifacts
|
||||
- storage
|
||||
- datalake
|
||||
maintainers:
|
||||
- name: Warehouse13 Team
|
||||
home: https://github.com/yourusername/warehouse13
|
||||
sources:
|
||||
- https://github.com/yourusername/warehouse13
|
||||
148
helm/warehouse13/QUICKSTART.md
Normal file
148
helm/warehouse13/QUICKSTART.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Warehouse13 Helm Chart - Quick Start
|
||||
|
||||
## 5-Minute Deployment
|
||||
|
||||
### Prerequisites Check
|
||||
|
||||
```bash
|
||||
# Verify Kubernetes cluster access
|
||||
kubectl cluster-info
|
||||
|
||||
# Verify Helm is installed
|
||||
helm version
|
||||
|
||||
# Create namespace
|
||||
kubectl create namespace warehouse13
|
||||
```
|
||||
|
||||
### Deploy with Defaults
|
||||
|
||||
```bash
|
||||
# Install chart
|
||||
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13
|
||||
|
||||
# Wait for ready
|
||||
kubectl wait --for=condition=ready pod --all -n warehouse13 --timeout=5m
|
||||
```
|
||||
|
||||
### Access Application
|
||||
|
||||
```bash
|
||||
# In separate terminals, run:
|
||||
|
||||
# Terminal 1: Frontend
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||
|
||||
# Terminal 2: API
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||
|
||||
# Terminal 3: MinIO Console
|
||||
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||
```
|
||||
|
||||
Then open in browser:
|
||||
- **Frontend:** http://localhost:4200
|
||||
- **API Docs:** http://localhost:8000/docs
|
||||
- **MinIO Console:** http://localhost:9001
|
||||
- Username: `minioadmin`
|
||||
- Password: `minioadmin`
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### 1. Development (No Persistence)
|
||||
|
||||
```bash
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--values ./helm/warehouse13/values-dev.yaml
|
||||
```
|
||||
|
||||
### 2. Production (With Ingress)
|
||||
|
||||
```bash
|
||||
# Update values-production.yaml with your settings first
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--values ./helm/warehouse13/values-production.yaml
|
||||
```
|
||||
|
||||
### 3. Air-Gapped (Custom Registry)
|
||||
|
||||
```bash
|
||||
# Update values-airgapped.yaml with your registry first
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--values ./helm/warehouse13/values-airgapped.yaml
|
||||
```
|
||||
|
||||
### 4. Custom Image Repository
|
||||
|
||||
```bash
|
||||
helm install warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--set postgres.image.repository=myregistry.com/postgres \
|
||||
--set minio.image.repository=myregistry.com/minio \
|
||||
--set api.image.repository=myregistry.com/warehouse13-api \
|
||||
--set frontend.image.repository=myregistry.com/warehouse13-frontend
|
||||
```
|
||||
|
||||
## Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check pods
|
||||
kubectl get pods -n warehouse13
|
||||
|
||||
# Check services
|
||||
kubectl get svc -n warehouse13
|
||||
|
||||
# View logs
|
||||
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=50
|
||||
|
||||
# Check resource usage
|
||||
kubectl top pods -n warehouse13
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
# Uninstall release
|
||||
helm uninstall warehouse13 -n warehouse13
|
||||
|
||||
# Delete PVCs (data will be lost!)
|
||||
kubectl delete pvc -n warehouse13 --all
|
||||
|
||||
# Delete namespace
|
||||
kubectl delete namespace warehouse13
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Full Documentation:** [README.md](./README.md)
|
||||
- **Deployment Guide:** [../../docs/HELM-DEPLOYMENT.md](../../docs/HELM-DEPLOYMENT.md)
|
||||
- **Configuration Options:** [values.yaml](./values.yaml)
|
||||
- **Example Configs:** [values-dev.yaml](./values-dev.yaml), [values-production.yaml](./values-production.yaml), [values-airgapped.yaml](./values-airgapped.yaml)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pods stuck in Pending
|
||||
```bash
|
||||
kubectl describe pod <pod-name> -n warehouse13
|
||||
# Check: PVC status, node resources, storage classes
|
||||
```
|
||||
|
||||
### Image pull errors
|
||||
```bash
|
||||
kubectl describe pod <pod-name> -n warehouse13
|
||||
# Check: Image repository, credentials, network access
|
||||
```
|
||||
|
||||
### Database connection errors
|
||||
```bash
|
||||
kubectl logs -n warehouse13 warehouse13-postgres-0
|
||||
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||
- Documentation: https://warehouse13.example.com/docs
|
||||
441
helm/warehouse13/README.md
Normal file
441
helm/warehouse13/README.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# Warehouse13 Helm Chart
|
||||
|
||||
Enterprise Test Artifact Storage - Kubernetes deployment via Helm
|
||||
|
||||
## Overview
|
||||
|
||||
This Helm chart deploys the complete Warehouse13 stack on Kubernetes:
|
||||
|
||||
- **PostgreSQL 15** - Metadata database
|
||||
- **MinIO** - S3-compatible object storage
|
||||
- **FastAPI Backend** - REST API server
|
||||
- **Angular Frontend** - Web UI (nginx-served)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.19+
|
||||
- Helm 3.0+
|
||||
- PV provisioner support (for persistent storage)
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Add the Warehouse13 chart repository (if published)
|
||||
helm repo add warehouse13 https://charts.warehouse13.example.com
|
||||
helm repo update
|
||||
|
||||
# Install with default values
|
||||
helm install my-warehouse13 warehouse13/warehouse13
|
||||
|
||||
# Or install from local chart
|
||||
helm install my-warehouse13 ./helm/warehouse13
|
||||
```
|
||||
|
||||
### Custom Installation
|
||||
|
||||
```bash
|
||||
# Install with custom values
|
||||
helm install my-warehouse13 ./helm/warehouse13 \
|
||||
--set postgres.persistence.size=20Gi \
|
||||
--set minio.persistence.size=100Gi \
|
||||
--set api.replicas=3
|
||||
|
||||
# Install in a specific namespace
|
||||
helm install my-warehouse13 ./helm/warehouse13 \
|
||||
--namespace warehouse13 \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Configurable Images
|
||||
|
||||
All component images can be customized via values.yaml or command-line flags:
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
image:
|
||||
repository: postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
minio:
|
||||
image:
|
||||
repository: minio/minio
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
api:
|
||||
image:
|
||||
repository: warehouse13/api
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: warehouse13/frontend
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
```
|
||||
|
||||
**Example: Using custom image registry**
|
||||
|
||||
```bash
|
||||
helm install my-warehouse13 ./helm/warehouse13 \
|
||||
--set postgres.image.repository=myregistry.example.com/postgres \
|
||||
--set minio.image.repository=myregistry.example.com/minio \
|
||||
--set api.image.repository=myregistry.example.com/warehouse13-api \
|
||||
--set frontend.image.repository=myregistry.example.com/warehouse13-frontend
|
||||
```
|
||||
|
||||
**Example: Air-gapped deployment with specific tags**
|
||||
|
||||
```bash
|
||||
helm install my-warehouse13 ./helm/warehouse13 \
|
||||
--set postgres.image.repository=harbor.internal/library/postgres \
|
||||
--set postgres.image.tag=15-alpine \
|
||||
--set minio.image.repository=harbor.internal/library/minio \
|
||||
--set minio.image.tag=RELEASE.2024-01-01T00-00-00Z \
|
||||
--set api.image.repository=harbor.internal/warehouse13/api \
|
||||
--set api.image.tag=v1.0.0 \
|
||||
--set frontend.image.repository=harbor.internal/warehouse13/frontend \
|
||||
--set frontend.image.tag=v1.0.0
|
||||
```
|
||||
|
||||
### Key Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `global.deploymentMode` | Deployment mode (standard/airgapped) | `standard` |
|
||||
| `global.storageBackend` | Storage backend (minio/s3) | `minio` |
|
||||
| `postgres.persistence.enabled` | Enable PostgreSQL persistence | `true` |
|
||||
| `postgres.persistence.size` | PostgreSQL PVC size | `10Gi` |
|
||||
| `postgres.auth.username` | PostgreSQL username | `user` |
|
||||
| `postgres.auth.password` | PostgreSQL password | `password` |
|
||||
| `minio.persistence.enabled` | Enable MinIO persistence | `true` |
|
||||
| `minio.persistence.size` | MinIO PVC size | `50Gi` |
|
||||
| `minio.auth.rootUser` | MinIO root username | `minioadmin` |
|
||||
| `minio.auth.rootPassword` | MinIO root password | `minioadmin` |
|
||||
| `api.replicas` | Number of API replicas | `2` |
|
||||
| `frontend.replicas` | Number of frontend replicas | `2` |
|
||||
| `ingress.enabled` | Enable ingress | `false` |
|
||||
| `ingress.className` | Ingress class name | `nginx` |
|
||||
| `ingress.hosts` | Ingress hosts configuration | See values.yaml |
|
||||
|
||||
### Example Configurations
|
||||
|
||||
#### Production with Ingress
|
||||
|
||||
```yaml
|
||||
# values-production.yaml
|
||||
global:
|
||||
deploymentMode: "standard"
|
||||
storageBackend: "minio"
|
||||
|
||||
postgres:
|
||||
persistence:
|
||||
size: 50Gi
|
||||
storageClass: "fast-ssd"
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
minio:
|
||||
persistence:
|
||||
size: 500Gi
|
||||
storageClass: "bulk-storage"
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2000m"
|
||||
|
||||
api:
|
||||
replicas: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
frontend:
|
||||
replicas: 3
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hosts:
|
||||
- host: warehouse13.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: frontend
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend: api
|
||||
tls:
|
||||
- secretName: warehouse13-tls
|
||||
hosts:
|
||||
- warehouse13.example.com
|
||||
```
|
||||
|
||||
```bash
|
||||
helm install my-warehouse13 ./helm/warehouse13 -f values-production.yaml
|
||||
```
|
||||
|
||||
#### Air-Gapped Environment
|
||||
|
||||
```yaml
|
||||
# values-airgapped.yaml
|
||||
global:
|
||||
deploymentMode: "airgapped"
|
||||
storageBackend: "minio"
|
||||
|
||||
postgres:
|
||||
image:
|
||||
repository: harbor.internal.example.com/library/postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
minio:
|
||||
image:
|
||||
repository: harbor.internal.example.com/library/minio
|
||||
tag: RELEASE.2024-01-01T00-00-00Z
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
api:
|
||||
image:
|
||||
repository: harbor.internal.example.com/warehouse13/api
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: harbor.internal.example.com/warehouse13/frontend
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
```
|
||||
|
||||
```bash
|
||||
helm install my-warehouse13 ./helm/warehouse13 -f values-airgapped.yaml
|
||||
```
|
||||
|
||||
#### Development/Testing
|
||||
|
||||
```yaml
|
||||
# values-dev.yaml
|
||||
global:
|
||||
deploymentMode: "standard"
|
||||
|
||||
postgres:
|
||||
persistence:
|
||||
enabled: false # Use emptyDir for quick testing
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
|
||||
minio:
|
||||
persistence:
|
||||
enabled: false
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
|
||||
api:
|
||||
replicas: 1
|
||||
image:
|
||||
tag: dev
|
||||
|
||||
frontend:
|
||||
replicas: 1
|
||||
image:
|
||||
tag: dev
|
||||
```
|
||||
|
||||
```bash
|
||||
helm install my-warehouse13 ./helm/warehouse13 -f values-dev.yaml
|
||||
```
|
||||
|
||||
## Accessing the Application
|
||||
|
||||
### Port Forwarding (Development)
|
||||
|
||||
```bash
|
||||
# Access frontend
|
||||
kubectl port-forward svc/warehouse13-frontend 4200:80
|
||||
|
||||
# Access API
|
||||
kubectl port-forward svc/warehouse13-api 8000:8000
|
||||
|
||||
# Access MinIO console
|
||||
kubectl port-forward svc/warehouse13-minio 9001:9001
|
||||
|
||||
# Then visit:
|
||||
# - Frontend: http://localhost:4200
|
||||
# - API: http://localhost:8000
|
||||
# - MinIO Console: http://localhost:9001
|
||||
```
|
||||
|
||||
### Via Ingress (Production)
|
||||
|
||||
If ingress is enabled:
|
||||
```
|
||||
https://warehouse13.example.com
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
# Upgrade with new values
|
||||
helm upgrade my-warehouse13 ./helm/warehouse13 \
|
||||
--set api.image.tag=v2.0.0 \
|
||||
--set frontend.image.tag=v2.0.0
|
||||
|
||||
# Upgrade with values file
|
||||
helm upgrade my-warehouse13 ./helm/warehouse13 -f values-production.yaml
|
||||
|
||||
# Upgrade and wait for completion
|
||||
helm upgrade my-warehouse13 ./helm/warehouse13 --wait --timeout 10m
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
# Uninstall the release
|
||||
helm uninstall my-warehouse13
|
||||
|
||||
# Note: PVCs are not deleted automatically. To delete them:
|
||||
kubectl delete pvc -l app.kubernetes.io/instance=my-warehouse13
|
||||
```
|
||||
|
||||
## Backup and Restore
|
||||
|
||||
### PostgreSQL Backup
|
||||
|
||||
```bash
|
||||
# Create backup
|
||||
kubectl exec -it warehouse13-postgres-0 -- pg_dump -U user warehouse13 > backup.sql
|
||||
|
||||
# Restore
|
||||
kubectl exec -i warehouse13-postgres-0 -- psql -U user warehouse13 < backup.sql
|
||||
```
|
||||
|
||||
### MinIO Backup
|
||||
|
||||
```bash
|
||||
# Install mc (MinIO Client)
|
||||
# Configure mc alias
|
||||
mc alias set w13 http://localhost:9001 minioadmin minioadmin
|
||||
|
||||
# Mirror bucket
|
||||
mc mirror w13/artifacts ./backup/artifacts
|
||||
|
||||
# Restore
|
||||
mc mirror ./backup/artifacts w13/artifacts
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Pod Status
|
||||
|
||||
```bash
|
||||
kubectl get pods -l app.kubernetes.io/name=warehouse13
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# API logs
|
||||
kubectl logs -l app.kubernetes.io/component=api -f
|
||||
|
||||
# Frontend logs
|
||||
kubectl logs -l app.kubernetes.io/component=frontend -f
|
||||
|
||||
# PostgreSQL logs
|
||||
kubectl logs warehouse13-postgres-0 -f
|
||||
|
||||
# MinIO logs
|
||||
kubectl logs warehouse13-minio-0 -f
|
||||
```
|
||||
|
||||
### Check Services
|
||||
|
||||
```bash
|
||||
kubectl get svc -l app.kubernetes.io/name=warehouse13
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Pods stuck in Pending**
|
||||
- Check PVC status: `kubectl get pvc`
|
||||
- Verify storage class exists: `kubectl get storageclass`
|
||||
- Check node resources: `kubectl describe nodes`
|
||||
|
||||
**Database connection errors**
|
||||
- Verify postgres pod is running: `kubectl get pod warehouse13-postgres-0`
|
||||
- Check database logs: `kubectl logs warehouse13-postgres-0`
|
||||
- Verify secret exists: `kubectl get secret warehouse13-secrets`
|
||||
|
||||
**Frontend cannot reach API**
|
||||
- Check ingress configuration: `kubectl describe ingress warehouse13-ingress`
|
||||
- Verify API service: `kubectl get svc warehouse13-api`
|
||||
- Check API pod health: `kubectl get pods -l app.kubernetes.io/component=api`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**Default credentials are for development only!** In production:
|
||||
|
||||
1. **Use external secrets management:**
|
||||
```yaml
|
||||
# Use sealed-secrets, external-secrets, or similar
|
||||
postgres:
|
||||
auth:
|
||||
username: "{{ .Values.externalSecrets.postgresUser }}"
|
||||
password: "{{ .Values.externalSecrets.postgresPassword }}"
|
||||
```
|
||||
|
||||
2. **Or create secrets manually:**
|
||||
```bash
|
||||
kubectl create secret generic warehouse13-secrets \
|
||||
--from-literal=postgres-username=secure-user \
|
||||
--from-literal=postgres-password=secure-password \
|
||||
--from-literal=minio-root-user=secure-minio-user \
|
||||
--from-literal=minio-root-password=secure-minio-password
|
||||
|
||||
# Then install without default secrets
|
||||
helm install my-warehouse13 ./helm/warehouse13 --set createSecrets=false
|
||||
```
|
||||
|
||||
3. **Enable TLS:**
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
- secretName: warehouse13-tls
|
||||
hosts:
|
||||
- warehouse13.example.com
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||
- Documentation: https://warehouse13.example.com/docs
|
||||
131
helm/warehouse13/templates/NOTES.txt
Normal file
131
helm/warehouse13/templates/NOTES.txt
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
_ _ _ _ _____
|
||||
| | | | | | / |___ /
|
||||
| | | | __ _ _ __ ___| |__ ___ _ _ ___ / / |_ \
|
||||
| |/\| |/ _` | '__/ _ \ '_ \ / _ \| | | / __|/ / ___) |
|
||||
\ /\ / (_| | | | __/ | | | (_) | |_| \__ \_/ |____/
|
||||
\/ \/ \__,_|_| \___|_| |_|\___/ \__,_|___(_)
|
||||
|
||||
Enterprise Test Artifact Storage has been deployed!
|
||||
|
||||
Chart Name: {{ .Chart.Name }}
|
||||
Chart Version: {{ .Chart.Version }}
|
||||
App Version: {{ .Chart.AppVersion }}
|
||||
|
||||
Release Name: {{ .Release.Name }}
|
||||
Namespace: {{ .Release.Namespace }}
|
||||
|
||||
---
|
||||
|
||||
DEPLOYMENT INFORMATION:
|
||||
|
||||
{{- if .Values.app.enabled }}
|
||||
Application (Unified API + Frontend):
|
||||
Service: warehouse13-app
|
||||
Replicas: {{ .Values.app.replicas }}
|
||||
Image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}
|
||||
Port: {{ .Values.app.service.port }}
|
||||
Note: Multi-stage build includes both Angular frontend and FastAPI backend
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.postgres.enabled }}
|
||||
PostgreSQL:
|
||||
Service: warehouse13-postgres
|
||||
Image: {{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}
|
||||
Persistence: {{ if .Values.postgres.persistence.enabled }}Enabled ({{ .Values.postgres.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.minio.enabled }}
|
||||
MinIO:
|
||||
Service: warehouse13-minio
|
||||
Image: {{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}
|
||||
Persistence: {{ if .Values.minio.persistence.enabled }}Enabled ({{ .Values.minio.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
|
||||
ACCESSING YOUR APPLICATION:
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
1. Via Ingress:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
https://{{ .host }}
|
||||
{{- end }}
|
||||
|
||||
{{- else }}
|
||||
|
||||
1. Using Port Forwarding:
|
||||
|
||||
# Application (Frontend + API)
|
||||
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-app 8000:8000
|
||||
Then visit:
|
||||
- Frontend: http://localhost:8000
|
||||
- API Docs: http://localhost:8000/docs
|
||||
- Health: http://localhost:8000/health
|
||||
|
||||
# MinIO Console
|
||||
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-minio 9001:9001
|
||||
Then visit: http://localhost:9001
|
||||
Username: {{ .Values.minio.auth.rootUser }}
|
||||
Password: {{ .Values.minio.auth.rootPassword }}
|
||||
|
||||
2. Expose via LoadBalancer or Ingress for external access.
|
||||
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
|
||||
CHECKING STATUS:
|
||||
|
||||
# View all pods
|
||||
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||
|
||||
# Check services
|
||||
kubectl get svc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||
|
||||
# View logs
|
||||
kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=app -f
|
||||
|
||||
---
|
||||
|
||||
UPGRADING:
|
||||
|
||||
helm upgrade {{ .Release.Name }} warehouse13/warehouse13 \
|
||||
--namespace {{ .Release.Namespace }}
|
||||
|
||||
---
|
||||
|
||||
UNINSTALLING:
|
||||
|
||||
helm uninstall {{ .Release.Name }} --namespace {{ .Release.Namespace }}
|
||||
|
||||
# Note: PVCs are retained. To delete them:
|
||||
kubectl delete pvc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||
|
||||
---
|
||||
|
||||
{{- if not .Values.ingress.enabled }}
|
||||
⚠️ IMPORTANT: Ingress is disabled. Enable it for production use:
|
||||
--set ingress.enabled=true
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.postgres.auth.password "password" }}
|
||||
⚠️ WARNING: Using default PostgreSQL password!
|
||||
For production, set a secure password:
|
||||
--set postgres.auth.password=YOUR_SECURE_PASSWORD
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.minio.auth.rootPassword "minioadmin" }}
|
||||
⚠️ WARNING: Using default MinIO password!
|
||||
For production, set a secure password:
|
||||
--set minio.auth.rootPassword=YOUR_SECURE_PASSWORD
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
|
||||
For more information, visit:
|
||||
Documentation: https://github.com/yourusername/warehouse13
|
||||
Issues: https://github.com/yourusername/warehouse13/issues
|
||||
|
||||
Thank you for using Warehouse13!
|
||||
@@ -1,14 +1,14 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "datalake.name" -}}
|
||||
{{- define "warehouse13.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "datalake.fullname" -}}
|
||||
{{- define "warehouse13.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "datalake.chart" -}}
|
||||
{{- define "warehouse13.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "datalake.labels" -}}
|
||||
helm.sh/chart: {{ include "datalake.chart" . }}
|
||||
{{ include "datalake.selectorLabels" . }}
|
||||
{{- define "warehouse13.labels" -}}
|
||||
helm.sh/chart: {{ include "warehouse13.chart" . }}
|
||||
{{ include "warehouse13.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
@@ -43,18 +43,29 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "datalake.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "datalake.name" . }}
|
||||
{{- define "warehouse13.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "datalake.serviceAccountName" -}}
|
||||
{{- define "warehouse13.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- default (include "warehouse13.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
PostgreSQL connection string
|
||||
*/}}
|
||||
{{- define "warehouse13.postgresUrl" -}}
|
||||
{{- if .Values.app.env.databaseUrl }}
|
||||
{{- .Values.app.env.databaseUrl }}
|
||||
{{- else }}
|
||||
{{- printf "postgresql://%s:%s@warehouse13-postgres:%d/%s" .Values.postgres.auth.username .Values.postgres.auth.password (.Values.postgres.service.port | int) .Values.postgres.auth.database }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
99
helm/warehouse13/templates/app-deployment.yaml
Normal file
99
helm/warehouse13/templates/app-deployment.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
{{- if .Values.app.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: warehouse13-app
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: app
|
||||
spec:
|
||||
replicas: {{ .Values.app.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: app
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitlab-dev-ns-registry-secret
|
||||
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: app
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: database-url
|
||||
- name: STORAGE_BACKEND
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: warehouse13-config
|
||||
key: STORAGE_BACKEND
|
||||
- name: MINIO_ENDPOINT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: warehouse13-config
|
||||
key: MINIO_ENDPOINT
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: minio-root-user
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: minio-root-password
|
||||
- name: MINIO_BUCKET_NAME
|
||||
value: "test-artifacts"
|
||||
- name: MINIO_SECURE
|
||||
value: "false"
|
||||
- name: DEPLOYMENT_MODE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: warehouse13-config
|
||||
key: DEPLOYMENT_MODE
|
||||
resources:
|
||||
{{- toYaml .Values.app.resources | nindent 10 }}
|
||||
{{- if .Values.app.healthCheck.enabled }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.app.healthCheck.liveness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.app.healthCheck.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.app.healthCheck.liveness.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .Values.app.healthCheck.readiness.path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.app.healthCheck.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.app.healthCheck.readiness.periodSeconds }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
19
helm/warehouse13/templates/app-service.yaml
Normal file
19
helm/warehouse13/templates/app-service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.app.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: warehouse13-app
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: app
|
||||
spec:
|
||||
type: {{ .Values.app.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.app.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: app
|
||||
{{- end }}
|
||||
10
helm/warehouse13/templates/configmap.yaml
Normal file
10
helm/warehouse13/templates/configmap.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: warehouse13-config
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
data:
|
||||
DEPLOYMENT_MODE: {{ .Values.global.deploymentMode | quote }}
|
||||
STORAGE_BACKEND: {{ .Values.global.storageBackend | quote }}
|
||||
MINIO_ENDPOINT: {{ printf "warehouse13-minio:%d" (.Values.minio.service.apiPort | int) | quote }}
|
||||
15
helm/warehouse13/templates/imagesecret.yaml
Normal file
15
helm/warehouse13/templates/imagesecret.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Values.imagePullSecret.name }}
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: {{
|
||||
(printf "{\"auths\":{\"%s\":{\"username\":\"%s\",\"password\":\"%s\",\"email\":\"%s\",\"auth\":\"%s\"}}}"
|
||||
.Values.imagePullSecret.server
|
||||
.Values.imagePullSecret.username
|
||||
.Values.imagePullSecret.password
|
||||
.Values.imagePullSecret.email
|
||||
(printf "%s" (b64enc (printf "%s:%s" .Values.imagePullSecret.username .Values.imagePullSecret.password))))
|
||||
| b64enc | quote
|
||||
}}
|
||||
@@ -2,9 +2,9 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "datalake.fullname" . }}
|
||||
name: warehouse13-ingress
|
||||
labels:
|
||||
{{- include "datalake.labels" . | nindent 4 }}
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
@@ -33,9 +33,9 @@ spec:
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "datalake.fullname" $ }}
|
||||
name: {{ printf "warehouse13-%s" .backend }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
number: {{ $.Values.app.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
23
helm/warehouse13/templates/minio-service.yaml
Normal file
23
helm/warehouse13/templates/minio-service.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
{{- if .Values.minio.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: warehouse13-minio
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: storage
|
||||
spec:
|
||||
type: {{ .Values.minio.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.minio.service.apiPort }}
|
||||
targetPort: api
|
||||
protocol: TCP
|
||||
name: api
|
||||
- port: {{ .Values.minio.service.consolePort }}
|
||||
targetPort: console
|
||||
protocol: TCP
|
||||
name: console
|
||||
selector:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: storage
|
||||
{{- end }}
|
||||
87
helm/warehouse13/templates/minio-statefulset.yaml
Normal file
87
helm/warehouse13/templates/minio-statefulset.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
{{- if .Values.minio.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: warehouse13-minio
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: storage
|
||||
spec:
|
||||
serviceName: warehouse13-minio
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: storage
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: storage
|
||||
spec:
|
||||
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: minio
|
||||
image: "{{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.minio.image.pullPolicy }}
|
||||
command:
|
||||
- minio
|
||||
- server
|
||||
- /data
|
||||
- --console-address
|
||||
- ":9001"
|
||||
ports:
|
||||
- name: api
|
||||
containerPort: 9000
|
||||
protocol: TCP
|
||||
- name: console
|
||||
containerPort: 9001
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: minio-root-user
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: minio-root-password
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
{{- toYaml .Values.minio.resources | nindent 10 }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/live
|
||||
port: api
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/ready
|
||||
port: api
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
{{- if .Values.minio.persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
{{- if .Values.minio.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.minio.persistence.storageClass }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.minio.persistence.size }}
|
||||
{{- else }}
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
19
helm/warehouse13/templates/postgres-service.yaml
Normal file
19
helm/warehouse13/templates/postgres-service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: warehouse13-postgres
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: database
|
||||
spec:
|
||||
type: {{ .Values.postgres.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.postgres.service.port }}
|
||||
targetPort: postgres
|
||||
protocol: TCP
|
||||
name: postgres
|
||||
selector:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: database
|
||||
{{- end }}
|
||||
89
helm/warehouse13/templates/postgres-statefulset.yaml
Normal file
89
helm/warehouse13/templates/postgres-statefulset.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: warehouse13-postgres
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: database
|
||||
spec:
|
||||
serviceName: warehouse13-postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: database
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: database
|
||||
spec:
|
||||
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: postgres
|
||||
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.postgres.image.pullPolicy }}
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: postgres-username
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: postgres-password
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: warehouse13-secrets
|
||||
key: postgres-database
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
resources:
|
||||
{{- toYaml .Values.postgres.resources | nindent 10 }}
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- $(POSTGRES_USER)
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- $(POSTGRES_USER)
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
{{- if .Values.postgres.persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
{{- if .Values.postgres.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.postgres.persistence.storageClass }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.postgres.persistence.size }}
|
||||
{{- else }}
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
14
helm/warehouse13/templates/secret.yaml
Normal file
14
helm/warehouse13/templates/secret.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: warehouse13-secrets
|
||||
labels:
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
postgres-username: {{ .Values.postgres.auth.username | quote }}
|
||||
postgres-password: {{ .Values.postgres.auth.password | quote }}
|
||||
postgres-database: {{ .Values.postgres.auth.database | quote }}
|
||||
minio-root-user: {{ .Values.minio.auth.rootUser | quote }}
|
||||
minio-root-password: {{ .Values.minio.auth.rootPassword | quote }}
|
||||
database-url: {{ include "warehouse13.postgresUrl" . | quote }}
|
||||
@@ -2,9 +2,9 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "datalake.serviceAccountName" . }}
|
||||
name: {{ include "warehouse13.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "datalake.labels" . | nindent 4 }}
|
||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
83
helm/warehouse13/values-airgapped.yaml
Normal file
83
helm/warehouse13/values-airgapped.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
# Warehouse13 - Air-Gapped Deployment Example
|
||||
# Use this for restricted/disconnected environments
|
||||
|
||||
global:
|
||||
deploymentMode: "airgapped"
|
||||
storageBackend: "minio"
|
||||
|
||||
# PostgreSQL with custom registry
|
||||
postgres:
|
||||
enabled: true
|
||||
image:
|
||||
repository: harbor.internal.example.com/library/postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: warehouse13user
|
||||
password: CHANGE_ME_SECURE_PASSWORD
|
||||
database: warehouse13
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 20Gi
|
||||
storageClass: "local-storage"
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
# MinIO with custom registry
|
||||
minio:
|
||||
enabled: true
|
||||
image:
|
||||
repository: harbor.internal.example.com/library/minio
|
||||
tag: RELEASE.2024-01-01T00-00-00Z
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
rootUser: CHANGE_ME_MINIO_USER
|
||||
rootPassword: CHANGE_ME_MINIO_PASSWORD
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 100Gi
|
||||
storageClass: "local-storage"
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
|
||||
# Application with custom registry (unified API + Frontend)
|
||||
app:
|
||||
enabled: true
|
||||
image:
|
||||
repository: harbor.internal.example.com/warehouse13/app
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 2
|
||||
resources:
|
||||
requests:
|
||||
memory: "768Mi"
|
||||
cpu: "750m"
|
||||
limits:
|
||||
memory: "1536Mi"
|
||||
cpu: "1500m"
|
||||
|
||||
# Ingress disabled for air-gapped - use NodePort or port-forward
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
# Node selector for specific nodes
|
||||
nodeSelector:
|
||||
environment: production
|
||||
storage: local
|
||||
|
||||
# Tolerations for tainted nodes
|
||||
tolerations:
|
||||
- key: "airgapped"
|
||||
operator: "Equal"
|
||||
value: "true"
|
||||
effect: "NoSchedule"
|
||||
69
helm/warehouse13/values-dev.yaml
Normal file
69
helm/warehouse13/values-dev.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
# Warehouse13 - Development/Testing Deployment Example
|
||||
# Use this for local testing or CI/CD environments
|
||||
|
||||
global:
|
||||
deploymentMode: "standard"
|
||||
storageBackend: "minio"
|
||||
|
||||
postgres:
|
||||
enabled: true
|
||||
image:
|
||||
repository: postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: dev
|
||||
password: dev
|
||||
database: warehouse13dev
|
||||
persistence:
|
||||
enabled: false # Use emptyDir for faster cleanup
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
|
||||
minio:
|
||||
enabled: true
|
||||
image:
|
||||
repository: minio/minio
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
rootUser: minioadmin
|
||||
rootPassword: minioadmin
|
||||
persistence:
|
||||
enabled: false # Use emptyDir for faster cleanup
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
|
||||
app:
|
||||
enabled: true
|
||||
image:
|
||||
repository: warehouse13/app
|
||||
tag: dev
|
||||
pullPolicy: Always # Always pull latest dev image
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
memory: "384Mi"
|
||||
cpu: "350m"
|
||||
limits:
|
||||
memory: "768Mi"
|
||||
cpu: "750m"
|
||||
healthCheck:
|
||||
enabled: true
|
||||
|
||||
ingress:
|
||||
enabled: false # Use port-forward for dev
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: "warehouse13-dev"
|
||||
98
helm/warehouse13/values-production.yaml
Normal file
98
helm/warehouse13/values-production.yaml
Normal file
@@ -0,0 +1,98 @@
|
||||
# Warehouse13 - Production Deployment Example
|
||||
# Use this for production environments with ingress and proper resources
|
||||
|
||||
global:
|
||||
deploymentMode: "standard"
|
||||
storageBackend: "minio"
|
||||
|
||||
postgres:
|
||||
enabled: true
|
||||
image:
|
||||
repository: postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
username: warehouse13user
|
||||
password: CHANGE_ME_SECURE_PASSWORD
|
||||
database: warehouse13
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 50Gi
|
||||
storageClass: "fast-ssd"
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
|
||||
minio:
|
||||
enabled: true
|
||||
image:
|
||||
repository: minio/minio
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
auth:
|
||||
rootUser: CHANGE_ME_MINIO_USER
|
||||
rootPassword: CHANGE_ME_MINIO_PASSWORD
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 500Gi
|
||||
storageClass: "bulk-storage"
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2000m"
|
||||
|
||||
app:
|
||||
enabled: true
|
||||
image:
|
||||
repository: warehouse13/app
|
||||
tag: v1.0.0
|
||||
pullPolicy: IfNotPresent
|
||||
replicas: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "768Mi"
|
||||
cpu: "750m"
|
||||
limits:
|
||||
memory: "1536Mi"
|
||||
cpu: "1500m"
|
||||
healthCheck:
|
||||
enabled: true
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
hosts:
|
||||
- host: warehouse13.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: app
|
||||
tls:
|
||||
- secretName: warehouse13-tls
|
||||
hosts:
|
||||
- warehouse13.example.com
|
||||
|
||||
# Affinity for pod distribution
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- warehouse13
|
||||
topologyKey: kubernetes.io/hostname
|
||||
148
helm/warehouse13/values.yaml
Normal file
148
helm/warehouse13/values.yaml
Normal file
@@ -0,0 +1,148 @@
|
||||
# Warehouse13 - Enterprise Test Artifact Storage
|
||||
# Default values for Helm chart
|
||||
|
||||
# Global settings
|
||||
global:
|
||||
deploymentMode: "air-gapped" # standard or airgapped
|
||||
storageBackend: "minio" # minio or s3
|
||||
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
enabled: true
|
||||
image:
|
||||
repository: containers.global.bsf.tools/postgres
|
||||
tag: 15-alpine
|
||||
pullPolicy: Always
|
||||
auth:
|
||||
username: user
|
||||
password: password
|
||||
database: warehouse13
|
||||
persistence:
|
||||
enabled: false
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 5432
|
||||
|
||||
# MinIO Object Storage
|
||||
minio:
|
||||
enabled: true
|
||||
image:
|
||||
repository: containers.global.bsf.tools/minio/minio
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
auth:
|
||||
rootUser: minioadmin
|
||||
rootPassword: minioadmin
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 50Gi
|
||||
storageClass: ""
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
service:
|
||||
type: ClusterIP
|
||||
apiPort: 9000
|
||||
consolePort: 9001
|
||||
|
||||
# Application (Unified API + Frontend)
|
||||
# The application uses a multi-stage Docker build:
|
||||
# - Stage 1: Builds Angular frontend
|
||||
# - Stage 2: Python FastAPI backend that serves the frontend from /static
|
||||
app:
|
||||
enabled: true
|
||||
image:
|
||||
repository: registry.global.bsf.tools/esv/bsf/bsf-services/warehouse13
|
||||
tag: main-7126c618
|
||||
pullPolicy: Always
|
||||
replicas: 1
|
||||
env:
|
||||
databaseUrl: "postgresql://user:password@warehouse13-postgres:5432/warehouse13"
|
||||
storageBackend: "minio"
|
||||
minioEndpoint: "warehouse13-minio:9000"
|
||||
resources:
|
||||
requests:
|
||||
memory: "768Mi"
|
||||
cpu: "350m"
|
||||
limits:
|
||||
memory: "768Mi"
|
||||
cpu: "750m"
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8000
|
||||
healthCheck:
|
||||
enabled: true
|
||||
liveness:
|
||||
path: /health
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readiness:
|
||||
path: /health
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
|
||||
# Ingress
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt"
|
||||
hosts:
|
||||
- host: warehouse13.common.global.bsf.tools
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend: app # All traffic goes to unified app (serves both API and frontend)
|
||||
tls:
|
||||
- secretName: warehouse13-tls
|
||||
hosts:
|
||||
- warehouse13.common.global.bsf.tools
|
||||
|
||||
# Service Account
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: "warehouse13"
|
||||
|
||||
# Pod Security
|
||||
podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
|
||||
# Node selector
|
||||
nodeSelector: {}
|
||||
|
||||
# Tolerations
|
||||
tolerations: []
|
||||
|
||||
# Affinity
|
||||
affinity: {}
|
||||
|
||||
imagePullSecret:
|
||||
name: gitlab-dev-ns-registry-secret
|
||||
username: project_9145_bot_imagepuller
|
||||
# Read only token so okay if hard coded here
|
||||
password: glpat-ZV7ASvBqFoiWC9QqD5WlTG86MQp1OjVxMgk.01.0z192vpfw
|
||||
server: registry.global.bsf.tools
|
||||
email: botemail@global.bsf.tool
|
||||
|
||||
74
quickstart-airgap.sh
Executable file
74
quickstart-airgap.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "Warehouse13 - Air-Gapped Quick Start"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "This script is for restricted/air-gapped environments"
|
||||
echo "where npm packages cannot be downloaded during Docker build."
|
||||
echo ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
echo "Error: Must run from project root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Error: Docker is not installed. Please install Docker first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker Compose is installed
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "Error: Docker Compose is not installed. Please install Docker Compose first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Building Angular frontend locally..."
|
||||
echo "==========================================="
|
||||
./scripts/build-for-airgap.sh
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Starting Docker containers..."
|
||||
echo "==========================================="
|
||||
docker-compose up -d --build
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Waiting for services to be ready..."
|
||||
sleep 15
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Services are running!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Web UI: http://localhost:8000"
|
||||
echo "API Docs: http://localhost:8000/docs"
|
||||
echo "MinIO Console: http://localhost:9001"
|
||||
echo " Username: minioadmin"
|
||||
echo " Password: minioadmin"
|
||||
echo ""
|
||||
echo "To view logs: docker-compose logs -f"
|
||||
echo "To stop: docker-compose down"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Testing the API..."
|
||||
echo "========================================="
|
||||
|
||||
# Wait a bit more for API to be fully ready
|
||||
sleep 5
|
||||
|
||||
# Test health endpoint
|
||||
if curl -s http://localhost:8000/health | grep -q "healthy"; then
|
||||
echo "✓ API is healthy!"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Setup complete! 🚀"
|
||||
echo "========================================="
|
||||
else
|
||||
echo "⚠ API is not responding yet. Please wait a moment and check http://localhost:8000/health"
|
||||
fi
|
||||
106
quickstart.bat
106
quickstart.bat
@@ -1,106 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo =========================================
|
||||
echo Obsidian - Quick Start
|
||||
echo =========================================
|
||||
echo.
|
||||
|
||||
REM Check if Docker is installed
|
||||
where docker >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Docker is not installed. Please install Docker Desktop first.
|
||||
echo Visit: https://www.docker.com/products/docker-desktop
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Docker Compose is available
|
||||
where docker-compose >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
REM Try docker compose (new version)
|
||||
docker compose version >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Docker Compose is not available.
|
||||
echo Please ensure Docker Desktop is running.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
set COMPOSE_CMD=docker compose
|
||||
) else (
|
||||
set COMPOSE_CMD=docker-compose
|
||||
)
|
||||
|
||||
REM Create .env file if it doesn't exist
|
||||
if not exist .env (
|
||||
echo Creating .env file from .env.example...
|
||||
copy .env.example .env >nul
|
||||
echo [OK] .env file created
|
||||
) else (
|
||||
echo [OK] .env file already exists
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Building and starting services with Docker Compose...
|
||||
%COMPOSE_CMD% up -d --build
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo Error: Failed to start services.
|
||||
echo Make sure Docker Desktop is running.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Waiting for services to be ready...
|
||||
timeout /t 15 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo =========================================
|
||||
echo Services are running!
|
||||
echo =========================================
|
||||
echo.
|
||||
echo Web UI: http://localhost:8000
|
||||
echo API Docs: http://localhost:8000/docs
|
||||
echo MinIO Console: http://localhost:9001
|
||||
echo Username: minioadmin
|
||||
echo Password: minioadmin
|
||||
echo.
|
||||
echo To view logs: %COMPOSE_CMD% logs -f
|
||||
echo To stop: %COMPOSE_CMD% down
|
||||
echo.
|
||||
echo =========================================
|
||||
echo Testing the API...
|
||||
echo =========================================
|
||||
echo.
|
||||
|
||||
REM Wait a bit more for API to be fully ready
|
||||
timeout /t 5 /nobreak >nul
|
||||
|
||||
REM Test health endpoint
|
||||
curl -s http://localhost:8000/health | findstr "healthy" >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
echo [OK] API is healthy!
|
||||
echo.
|
||||
echo =========================================
|
||||
echo Open your browser to get started:
|
||||
echo http://localhost:8000
|
||||
echo =========================================
|
||||
) else (
|
||||
echo [WARNING] API is not responding yet.
|
||||
echo Please wait a moment and check http://localhost:8000
|
||||
)
|
||||
|
||||
echo.
|
||||
echo =========================================
|
||||
echo Setup complete!
|
||||
echo =========================================
|
||||
echo.
|
||||
echo Press any key to open the UI in your browser...
|
||||
pause >nul
|
||||
|
||||
REM Open browser
|
||||
start http://localhost:8000
|
||||
|
||||
exit /b 0
|
||||
@@ -1,7 +1,7 @@
|
||||
# Test Artifact Data Lake - Quick Start (PowerShell)
|
||||
|
||||
Write-Host "=========================================" -ForegroundColor Cyan
|
||||
Write-Host "Obsidian - Quick Start" -ForegroundColor Cyan
|
||||
Write-Host "Warehouse13 - Quick Start" -ForegroundColor Cyan
|
||||
Write-Host "=========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
@@ -121,7 +121,7 @@ Write-Host "Useful Commands:" -ForegroundColor Cyan
|
||||
Write-Host " Generate seed data: " -NoNewline
|
||||
Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow
|
||||
Write-Host " View logs: " -NoNewline
|
||||
Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow
|
||||
Write-Host "$composeCmd logs -f app" -ForegroundColor Yellow
|
||||
Write-Host " Restart services: " -NoNewline
|
||||
Write-Host "$composeCmd restart" -ForegroundColor Yellow
|
||||
Write-Host " Stop all: " -NoNewline
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "Obsidian - Quick Start"
|
||||
echo "Warehouse13 - Quick Start"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
@@ -41,7 +41,7 @@ echo "========================================="
|
||||
echo "Services are running!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "API: http://localhost:8000"
|
||||
echo "Web UI: http://localhost:8000"
|
||||
echo "API Docs: http://localhost:8000/docs"
|
||||
echo "MinIO Console: http://localhost:9001"
|
||||
echo " Username: minioadmin"
|
||||
|
||||
70
scripts/build-for-airgap.sh
Normal file
70
scripts/build-for-airgap.sh
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "Building Angular for Air-Gapped Deployment"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
echo "Error: Must run from project root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if node is installed
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "Error: Node.js is not installed. Please install Node.js 24+ first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if npm is installed
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "Error: npm is not installed. Please install npm first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1/3: Installing dependencies..."
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
echo ""
|
||||
echo "Step 2/3: Building Angular production bundle..."
|
||||
npm run build:prod
|
||||
|
||||
echo ""
|
||||
echo "Step 3/3: Copying to static directory..."
|
||||
if [ -d "dist/frontend/browser" ]; then
|
||||
echo "✓ Build successful!"
|
||||
echo "✓ Output: frontend/dist/frontend/browser"
|
||||
|
||||
# Copy to static directory for local FastAPI serving
|
||||
cd ..
|
||||
rm -rf static/*
|
||||
cp -r frontend/dist/frontend/browser/* static/
|
||||
echo "✓ Copied to static/ directory"
|
||||
|
||||
ls -lh static/ | head -10
|
||||
else
|
||||
echo "✗ Build failed - output directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Build Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "The Angular app has been built and copied to static/"
|
||||
echo "You can now:"
|
||||
echo ""
|
||||
echo "1. Run locally with FastAPI:"
|
||||
echo " uvicorn app.main:app --reload"
|
||||
echo " Access at: http://localhost:8000"
|
||||
echo ""
|
||||
echo "2. Deploy with Docker:"
|
||||
echo " docker-compose up -d --build"
|
||||
echo " (Docker will rebuild Angular during build)"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
146
scripts/check-package-age.js
Executable file
146
scripts/check-package-age.js
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Check if npm packages are at least 2 weeks old before installation
|
||||
* Usage: node scripts/check-package-age.js
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function getPackageInfo(packageName, version) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Remove version prefixes like ^, ~, >=
|
||||
let cleanVersion = version.replace(/^[\^~>=]+/, '');
|
||||
|
||||
// If version contains .x or wildcards, fetch all versions and find latest matching
|
||||
if (cleanVersion.includes('x') || cleanVersion.includes('*')) {
|
||||
const url = `https://registry.npmjs.org/${packageName}`;
|
||||
|
||||
https.get(url, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const info = JSON.parse(data);
|
||||
const versions = Object.keys(info.versions || {});
|
||||
|
||||
// Convert version pattern to regex (e.g., "19.2.x" -> /^19\.2\.\d+$/)
|
||||
const pattern = cleanVersion.replace(/\./g, '\\.').replace(/x|\*/g, '\\d+');
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
|
||||
// Find matching versions and get the latest
|
||||
const matchingVersions = versions.filter(v => regex.test(v)).sort();
|
||||
const latestMatching = matchingVersions[matchingVersions.length - 1];
|
||||
|
||||
if (latestMatching && info.time && info.time[latestMatching]) {
|
||||
resolve({
|
||||
name: packageName,
|
||||
version: latestMatching,
|
||||
published: info.time[latestMatching],
|
||||
error: null
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`No matching version found for ${packageName}@${cleanVersion}`));
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error(`Failed to parse response for ${packageName}@${cleanVersion}`));
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
// Exact version lookup
|
||||
const url = `https://registry.npmjs.org/${packageName}/${cleanVersion}`;
|
||||
|
||||
https.get(url, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const info = JSON.parse(data);
|
||||
resolve({
|
||||
name: packageName,
|
||||
version: cleanVersion,
|
||||
published: info.time || info._time,
|
||||
error: null
|
||||
});
|
||||
} catch (err) {
|
||||
reject(new Error(`Failed to parse response for ${packageName}@${cleanVersion}`));
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkPackageAge() {
|
||||
const packageJsonPath = path.join(__dirname, '../frontend/package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
console.log('Checking package ages (must be at least 2 weeks old)...\n');
|
||||
|
||||
const tooNew = [];
|
||||
const errors = [];
|
||||
|
||||
for (const [name, version] of Object.entries(allDeps)) {
|
||||
try {
|
||||
const info = await getPackageInfo(name, version);
|
||||
const publishDate = new Date(info.published);
|
||||
const age = Date.now() - publishDate.getTime();
|
||||
const ageInDays = Math.floor(age / (24 * 60 * 60 * 1000));
|
||||
|
||||
if (age < TWO_WEEKS_MS) {
|
||||
tooNew.push({
|
||||
name,
|
||||
version: info.version,
|
||||
age: ageInDays,
|
||||
published: publishDate.toISOString().split('T')[0]
|
||||
});
|
||||
console.log(`❌ ${name}@${info.version} - ${ageInDays} days old (published ${publishDate.toISOString().split('T')[0]})`);
|
||||
} else {
|
||||
console.log(`✓ ${name}@${info.version} - ${ageInDays} days old`);
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ name, version, error: err.message });
|
||||
console.log(`⚠️ ${name}@${version} - Could not check: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
|
||||
if (tooNew.length > 0) {
|
||||
console.log(`\n❌ FAILED: ${tooNew.length} package(s) are newer than 2 weeks:\n`);
|
||||
tooNew.forEach(pkg => {
|
||||
console.log(` - ${pkg.name}@${pkg.version} (${pkg.age} days old, published ${pkg.published})`);
|
||||
});
|
||||
process.exit(1);
|
||||
} else if (errors.length > 0) {
|
||||
console.log(`\n⚠️ WARNING: Could not verify ${errors.length} package(s)`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n✓ SUCCESS: All packages are at least 2 weeks old');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
checkPackageAge();
|
||||
62
scripts/check-ready.sh
Executable file
62
scripts/check-ready.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "========================================="
|
||||
echo "Warehouse13 - Readiness Check"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
errors=0
|
||||
|
||||
# Check if pre-built files exist
|
||||
if [ -d "frontend/dist/frontend/browser" ]; then
|
||||
echo "✓ Pre-built Angular files found"
|
||||
echo " Location: frontend/dist/frontend/browser"
|
||||
file_count=$(ls -1 frontend/dist/frontend/browser | wc -l)
|
||||
echo " Files: $file_count"
|
||||
else
|
||||
echo "✗ ERROR: Pre-built Angular files NOT found!"
|
||||
echo " Expected: frontend/dist/frontend/browser"
|
||||
echo ""
|
||||
echo " You need to build the Angular app first:"
|
||||
echo " Run: ./scripts/build-for-airgap.sh"
|
||||
echo " OR: cd frontend && npm install && npm run build:prod"
|
||||
echo ""
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if docker-compose.yml is configured for pre-built
|
||||
if grep -q "Dockerfile.frontend.prebuilt" docker-compose.yml; then
|
||||
echo "✓ docker-compose.yml configured for pre-built deployment"
|
||||
else
|
||||
echo "⚠ WARNING: docker-compose.yml may not be configured for pre-built deployment"
|
||||
echo " Current frontend dockerfile:"
|
||||
grep "dockerfile:" docker-compose.yml | grep -A 1 "frontend:" | tail -1
|
||||
echo ""
|
||||
echo " For air-gapped deployment, it should be: Dockerfile.frontend.prebuilt"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if Docker is running
|
||||
if docker info &> /dev/null; then
|
||||
echo "✓ Docker is running"
|
||||
else
|
||||
echo "✗ ERROR: Docker is not running or not accessible"
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
|
||||
if [ $errors -eq 0 ]; then
|
||||
echo "✓ Ready to deploy!"
|
||||
echo ""
|
||||
echo "Run: docker-compose up -d --build"
|
||||
echo "Or: ./quickstart-airgap.sh"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ NOT ready - please fix the errors above"
|
||||
exit 1
|
||||
fi
|
||||
41
scripts/pin-old-versions.sh
Executable file
41
scripts/pin-old-versions.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Pin npm packages to versions that are at least 2 weeks old
|
||||
# This script helps ensure compliance with package age requirements
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================="
|
||||
echo "Pin NPM Packages to Old Versions"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
cd frontend
|
||||
|
||||
echo "Step 1: Checking current package ages..."
|
||||
node ../scripts/check-package-age.js || {
|
||||
echo ""
|
||||
echo "Some packages are too new. Recommendations:"
|
||||
echo "1. Manually downgrade packages in package.json to older versions"
|
||||
echo "2. Run: npm install --package-lock-only to update lock file"
|
||||
echo "3. Re-run this script to verify"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Ensuring package-lock.json uses exact versions..."
|
||||
if [ -f "package-lock.json" ]; then
|
||||
echo "✓ package-lock.json exists"
|
||||
else
|
||||
echo "⚠ package-lock.json does not exist. Creating it..."
|
||||
npm install --package-lock-only
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "✓ All packages meet the 2-week age requirement"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "To install these packages:"
|
||||
echo " npm ci # Uses exact versions from package-lock.json"
|
||||
echo ""
|
||||
@@ -1,251 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Obsidian - Test Artifact Data Lake</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>◆ Obsidian</h1>
|
||||
<div class="header-info">
|
||||
<span id="deployment-mode" class="badge"></span>
|
||||
<span id="storage-backend" class="badge"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs">
|
||||
<button class="tab-button active" onclick="showTab('artifacts')">
|
||||
<i data-lucide="database" style="width: 16px; height: 16px;"></i> Artifacts
|
||||
</button>
|
||||
<button class="tab-button" onclick="showTab('upload')">
|
||||
<i data-lucide="upload" style="width: 16px; height: 16px;"></i> Upload
|
||||
</button>
|
||||
<button class="tab-button" onclick="showTab('query')">
|
||||
<i data-lucide="search" style="width: 16px; height: 16px;"></i> Query
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Artifacts Tab -->
|
||||
<div id="artifacts-tab" class="tab-content active">
|
||||
<div class="toolbar">
|
||||
<button onclick="loadArtifacts()" class="btn btn-primary">
|
||||
<i data-lucide="refresh-cw" style="width: 16px; height: 16px;"></i> Refresh
|
||||
</button>
|
||||
<button id="auto-refresh-toggle" onclick="toggleAutoRefresh()" class="btn btn-success">
|
||||
Auto-refresh: ON
|
||||
</button>
|
||||
<button onclick="generateSeedData()" class="btn btn-secondary">
|
||||
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data
|
||||
</button>
|
||||
|
||||
<span id="artifact-count" class="count-badge"></span>
|
||||
|
||||
<div class="filter-inline">
|
||||
<i data-lucide="search" style="width: 16px; height: 16px; color: #64748b;"></i>
|
||||
<input type="text" id="filter-search" placeholder="Search..." oninput="filterTable()">
|
||||
<button onclick="clearFilters()" class="btn-clear" title="Clear search">
|
||||
<i data-lucide="x" style="width: 14px; height: 14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table id="artifacts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" onclick="sortTable('test_suite')">
|
||||
Sim Source <span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('filename')">
|
||||
Artifacts <span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('created_at')">
|
||||
Date <span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th class="sortable" onclick="sortTable('test_name')">
|
||||
Uploaded By <span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="artifacts-tbody">
|
||||
<tr>
|
||||
<td colspan="5" class="loading">Loading artifacts...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button onclick="previousPage()" id="prev-btn" class="btn">← Previous</button>
|
||||
<span id="page-info">Page 1</span>
|
||||
<button onclick="nextPage()" id="next-btn" class="btn">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Tab -->
|
||||
<div id="upload-tab" class="tab-content">
|
||||
<div class="upload-section">
|
||||
<h2>Upload Artifact</h2>
|
||||
<form id="upload-form" onsubmit="uploadArtifact(event)">
|
||||
<div class="form-group">
|
||||
<label for="file">File *</label>
|
||||
<input type="file" id="file" name="file" required>
|
||||
<small>Supported: CSV, JSON, binary files, PCAP</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sim-source">Sim Source *</label>
|
||||
<input type="text" id="sim-source" name="test_suite" placeholder="e.g., Jenkins, GitLab CI" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="uploaded-by">Uploaded By *</label>
|
||||
<input type="text" id="uploaded-by" name="test_name" placeholder="e.g., john.doe" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="sim-source-id">SIM Source ID (for grouping)</label>
|
||||
<input type="text" id="sim-source-id" name="sim_source_id" placeholder="e.g., sim_run_20251015_001">
|
||||
<small>Use same ID for multiple artifacts from same source</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags (comma-separated) *</label>
|
||||
<input type="text" id="tags" name="tags" placeholder="e.g., regression, smoke, critical" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="test-result">Test Result</label>
|
||||
<select id="test-result" name="test_result">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="pass">Pass</option>
|
||||
<option value="fail">Fail</option>
|
||||
<option value="skip">Skip</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version">Version</label>
|
||||
<input type="text" id="version" name="version" placeholder="e.g., v1.0.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="3" placeholder="Describe this artifact..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="test-config">Test Config (JSON)</label>
|
||||
<textarea id="test-config" name="test_config" rows="4" placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="custom-metadata">Custom Metadata (JSON)</label>
|
||||
<textarea id="custom-metadata" name="custom_metadata" rows="4" placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large">
|
||||
<i data-lucide="upload" style="width: 18px; height: 18px;"></i> Upload Artifact
|
||||
</button>
|
||||
</form>
|
||||
<div id="upload-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Query Tab -->
|
||||
<div id="query-tab" class="tab-content">
|
||||
<div class="query-section">
|
||||
<h2>Query Artifacts</h2>
|
||||
<form id="query-form" onsubmit="queryArtifacts(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-filename">Filename</label>
|
||||
<input type="text" id="q-filename" placeholder="Search filename...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-type">File Type</label>
|
||||
<select id="q-type">
|
||||
<option value="">All</option>
|
||||
<option value="csv">CSV</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">Binary</option>
|
||||
<option value="pcap">PCAP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-test-name">Test Name</label>
|
||||
<input type="text" id="q-test-name" placeholder="Search test name...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-suite">Test Suite</label>
|
||||
<input type="text" id="q-suite" placeholder="e.g., integration">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-result">Test Result</label>
|
||||
<select id="q-result">
|
||||
<option value="">All</option>
|
||||
<option value="pass">Pass</option>
|
||||
<option value="fail">Fail</option>
|
||||
<option value="skip">Skip</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-tags">Tags (comma-separated)</label>
|
||||
<input type="text" id="q-tags" placeholder="e.g., regression, smoke">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="q-start-date">Start Date</label>
|
||||
<input type="datetime-local" id="q-start-date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="q-end-date">End Date</label>
|
||||
<input type="datetime-local" id="q-end-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-large">
|
||||
<i data-lucide="search" style="width: 18px; height: 18px;"></i> Search
|
||||
</button>
|
||||
<button type="button" onclick="clearQuery()" class="btn btn-secondary">
|
||||
<i data-lucide="x" style="width: 18px; height: 18px;"></i> Clear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artifact Detail Modal -->
|
||||
<div id="detail-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeDetailModal()">×</span>
|
||||
<h2>Artifact Details</h2>
|
||||
<div id="detail-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
592
static/js/app.js
592
static/js/app.js
@@ -1,592 +0,0 @@
|
||||
// API Base URL
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// Pagination
|
||||
let currentPage = 1;
|
||||
let pageSize = 25;
|
||||
let totalArtifacts = 0;
|
||||
|
||||
// Auto-refresh
|
||||
let autoRefreshEnabled = true;
|
||||
let autoRefreshInterval = null;
|
||||
const REFRESH_INTERVAL_MS = 5000; // 5 seconds
|
||||
|
||||
// Sorting and filtering
|
||||
let allArtifacts = []; // Store all artifacts for client-side sorting/filtering
|
||||
let currentSortColumn = null;
|
||||
let currentSortDirection = 'asc';
|
||||
|
||||
// Load API info on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadApiInfo();
|
||||
loadArtifacts();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// Load API information
|
||||
async function loadApiInfo() {
|
||||
try {
|
||||
const response = await fetch('/api');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('deployment-mode').textContent = `Mode: ${data.deployment_mode}`;
|
||||
document.getElementById('storage-backend').textContent = `Storage: ${data.storage_backend}`;
|
||||
} catch (error) {
|
||||
console.error('Error loading API info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load artifacts
|
||||
async function loadArtifacts(limit = pageSize, offset = 0) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`);
|
||||
const artifacts = await response.json();
|
||||
|
||||
allArtifacts = artifacts; // Store for sorting/filtering
|
||||
displayArtifacts(artifacts);
|
||||
updatePagination(artifacts.length);
|
||||
} catch (error) {
|
||||
console.error('Error loading artifacts:', error);
|
||||
document.getElementById('artifacts-tbody').innerHTML = `
|
||||
<tr><td colspan="5" class="loading" style="color: #ef4444;">
|
||||
Error loading artifacts: ${error.message}
|
||||
</td></tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Display artifacts in table
|
||||
function displayArtifacts(artifacts) {
|
||||
const tbody = document.getElementById('artifacts-tbody');
|
||||
|
||||
if (artifacts.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="loading">No artifacts found. Upload some files to get started!</td></tr>';
|
||||
document.getElementById('artifact-count').textContent = '0 artifacts';
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply current sort if active
|
||||
let displayedArtifacts = artifacts;
|
||||
if (currentSortColumn) {
|
||||
displayedArtifacts = applySorting([...artifacts]);
|
||||
}
|
||||
|
||||
tbody.innerHTML = displayedArtifacts.map(artifact => `
|
||||
<tr>
|
||||
<td>${artifact.sim_source_id || artifact.test_suite || '-'}</td>
|
||||
<td>
|
||||
<a href="#" onclick="showDetail(${artifact.id}); return false;" style="color: #60a5fa; text-decoration: none;">
|
||||
${escapeHtml(artifact.filename)}
|
||||
</a>
|
||||
${artifact.tags && artifact.tags.length > 0 ? `<br><div style="margin-top: 5px;">${formatTags(artifact.tags)}</div>` : ''}
|
||||
</td>
|
||||
<td>${formatDate(artifact.created_at)}</td>
|
||||
<td>${artifact.test_name || '-'}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="icon-btn" onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" title="Download">
|
||||
<i data-lucide="download" style="width: 16px; height: 16px;"></i>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="deleteArtifact(${artifact.id})" title="Delete">
|
||||
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('artifact-count').textContent = `${displayedArtifacts.length} artifacts`;
|
||||
|
||||
// Re-initialize Lucide icons for dynamically added content
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// Format result badge
|
||||
function formatResult(result) {
|
||||
if (!result) return '-';
|
||||
const className = `result-badge result-${result}`;
|
||||
return `<span class="${className}">${result}</span>`;
|
||||
}
|
||||
|
||||
// Format tags
|
||||
function formatTags(tags) {
|
||||
if (!tags || tags.length === 0) return '-';
|
||||
return tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join(' ');
|
||||
}
|
||||
|
||||
// Format bytes
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Show artifact detail
|
||||
async function showDetail(id) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/${id}`);
|
||||
const artifact = await response.json();
|
||||
|
||||
const detailContent = document.getElementById('detail-content');
|
||||
detailContent.innerHTML = `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">ID</div>
|
||||
<div class="detail-value">${artifact.id}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Filename</div>
|
||||
<div class="detail-value">${escapeHtml(artifact.filename)}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">File Type</div>
|
||||
<div class="detail-value"><span class="file-type-badge">${artifact.file_type}</span></div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Size</div>
|
||||
<div class="detail-value">${formatBytes(artifact.file_size)}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Storage Path</div>
|
||||
<div class="detail-value"><code>${artifact.storage_path}</code></div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Uploaded By</div>
|
||||
<div class="detail-value">${artifact.test_name || '-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Sim Source</div>
|
||||
<div class="detail-value">${artifact.test_suite || '-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Test Result</div>
|
||||
<div class="detail-value">${formatResult(artifact.test_result)}</div>
|
||||
</div>
|
||||
${artifact.test_config ? `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Test Config</div>
|
||||
<div class="detail-value"><pre>${JSON.stringify(artifact.test_config, null, 2)}</pre></div>
|
||||
</div>
|
||||
` : ''}
|
||||
${artifact.custom_metadata ? `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Custom Metadata</div>
|
||||
<div class="detail-value"><pre>${JSON.stringify(artifact.custom_metadata, null, 2)}</pre></div>
|
||||
</div>
|
||||
` : ''}
|
||||
${artifact.description ? `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Description</div>
|
||||
<div class="detail-value">${escapeHtml(artifact.description)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${artifact.tags && artifact.tags.length > 0 ? `
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Tags</div>
|
||||
<div class="detail-value">${formatTags(artifact.tags)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Version</div>
|
||||
<div class="detail-value">${artifact.version || '-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Created</div>
|
||||
<div class="detail-value">${formatDate(artifact.created_at)}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Updated</div>
|
||||
<div class="detail-value">${formatDate(artifact.updated_at)}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px; display: flex; gap: 10px;">
|
||||
<button onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" class="btn btn-primary">
|
||||
<i data-lucide="download" style="width: 16px; height: 16px;"></i> Download
|
||||
</button>
|
||||
<button onclick="deleteArtifact(${artifact.id}); closeDetailModal();" class="btn btn-danger">
|
||||
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('detail-modal').classList.add('active');
|
||||
|
||||
// Re-initialize Lucide icons for modal content
|
||||
lucide.createIcons();
|
||||
} catch (error) {
|
||||
alert('Error loading artifact details: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Close detail modal
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detail-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Download artifact
|
||||
async function downloadArtifact(id, filename) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/${id}/download`);
|
||||
const blob = await response.blob();
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
alert('Error downloading artifact: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete artifact
|
||||
async function deleteArtifact(id) {
|
||||
if (!confirm('Are you sure you want to delete this artifact? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadArtifacts((currentPage - 1) * pageSize, pageSize);
|
||||
alert('Artifact deleted successfully');
|
||||
} else {
|
||||
throw new Error('Failed to delete artifact');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error deleting artifact: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Upload artifact
|
||||
async function uploadArtifact(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData();
|
||||
|
||||
// Add file
|
||||
const fileInput = document.getElementById('file');
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
// Add optional fields
|
||||
const fields = ['test_name', 'test_suite', 'test_result', 'version', 'description', 'sim_source_id'];
|
||||
fields.forEach(field => {
|
||||
const value = form.elements[field]?.value;
|
||||
if (value) formData.append(field, value);
|
||||
});
|
||||
|
||||
// Add tags (convert comma-separated to JSON array)
|
||||
const tags = document.getElementById('tags').value;
|
||||
if (tags) {
|
||||
const tagsArray = tags.split(',').map(t => t.trim()).filter(t => t);
|
||||
formData.append('tags', JSON.stringify(tagsArray));
|
||||
}
|
||||
|
||||
// Add JSON fields
|
||||
const testConfig = document.getElementById('test-config').value;
|
||||
if (testConfig) {
|
||||
try {
|
||||
JSON.parse(testConfig); // Validate
|
||||
formData.append('test_config', testConfig);
|
||||
} catch (e) {
|
||||
showUploadStatus('Invalid Test Config JSON', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const customMetadata = document.getElementById('custom-metadata').value;
|
||||
if (customMetadata) {
|
||||
try {
|
||||
JSON.parse(customMetadata); // Validate
|
||||
formData.append('custom_metadata', customMetadata);
|
||||
} catch (e) {
|
||||
showUploadStatus('Invalid Custom Metadata JSON', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const artifact = await response.json();
|
||||
showUploadStatus(`Successfully uploaded: ${artifact.filename}`, true);
|
||||
form.reset();
|
||||
loadArtifacts();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
showUploadStatus('Upload failed: ' + error.message, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Show upload status
|
||||
function showUploadStatus(message, success) {
|
||||
const status = document.getElementById('upload-status');
|
||||
status.textContent = message;
|
||||
status.className = success ? 'success' : 'error';
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Query artifacts
|
||||
async function queryArtifacts(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const query = {};
|
||||
|
||||
const filename = document.getElementById('q-filename').value;
|
||||
if (filename) query.filename = filename;
|
||||
|
||||
const fileType = document.getElementById('q-type').value;
|
||||
if (fileType) query.file_type = fileType;
|
||||
|
||||
const testName = document.getElementById('q-test-name').value;
|
||||
if (testName) query.test_name = testName;
|
||||
|
||||
const suite = document.getElementById('q-suite').value;
|
||||
if (suite) query.test_suite = suite;
|
||||
|
||||
const result = document.getElementById('q-result').value;
|
||||
if (result) query.test_result = result;
|
||||
|
||||
const tags = document.getElementById('q-tags').value;
|
||||
if (tags) {
|
||||
query.tags = tags.split(',').map(t => t.trim()).filter(t => t);
|
||||
}
|
||||
|
||||
const startDate = document.getElementById('q-start-date').value;
|
||||
if (startDate) query.start_date = new Date(startDate).toISOString();
|
||||
|
||||
const endDate = document.getElementById('q-end-date').value;
|
||||
if (endDate) query.end_date = new Date(endDate).toISOString();
|
||||
|
||||
query.limit = 100;
|
||||
query.offset = 0;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/artifacts/query`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(query)
|
||||
});
|
||||
|
||||
const artifacts = await response.json();
|
||||
|
||||
// Switch to artifacts tab and display results
|
||||
showTab('artifacts');
|
||||
displayArtifacts(artifacts);
|
||||
} catch (error) {
|
||||
alert('Query failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear query form
|
||||
function clearQuery() {
|
||||
document.getElementById('query-form').reset();
|
||||
}
|
||||
|
||||
// Generate seed data
|
||||
async function generateSeedData() {
|
||||
const count = prompt('How many artifacts to generate? (1-100)', '10');
|
||||
if (!count) return;
|
||||
|
||||
const num = parseInt(count);
|
||||
if (isNaN(num) || num < 1 || num > 100) {
|
||||
alert('Please enter a number between 1 and 100');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/seed/generate/${num}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert(result.message);
|
||||
loadArtifacts();
|
||||
} else {
|
||||
throw new Error(result.detail || 'Generation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error generating seed data: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Tab navigation
|
||||
function showTab(tabName) {
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
|
||||
// Pagination
|
||||
function updatePagination(count) {
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
pageInfo.textContent = `Page ${currentPage}`;
|
||||
|
||||
document.getElementById('prev-btn').disabled = currentPage === 1;
|
||||
document.getElementById('next-btn').disabled = count < pageSize;
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
currentPage++;
|
||||
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
|
||||
}
|
||||
|
||||
// Auto-refresh functions
|
||||
function startAutoRefresh() {
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
}
|
||||
|
||||
if (autoRefreshEnabled) {
|
||||
autoRefreshInterval = setInterval(() => {
|
||||
// Only refresh if on the artifacts tab
|
||||
const artifactsTab = document.getElementById('artifacts-tab');
|
||||
if (artifactsTab && artifactsTab.classList.contains('active')) {
|
||||
loadArtifacts(pageSize, (currentPage - 1) * pageSize);
|
||||
}
|
||||
}, REFRESH_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
|
||||
const toggleBtn = document.getElementById('auto-refresh-toggle');
|
||||
if (autoRefreshEnabled) {
|
||||
toggleBtn.textContent = 'Auto-refresh: ON';
|
||||
toggleBtn.classList.remove('btn-secondary');
|
||||
toggleBtn.classList.add('btn-success');
|
||||
startAutoRefresh();
|
||||
} else {
|
||||
toggleBtn.textContent = 'Auto-refresh: OFF';
|
||||
toggleBtn.classList.remove('btn-success');
|
||||
toggleBtn.classList.add('btn-secondary');
|
||||
if (autoRefreshInterval) {
|
||||
clearInterval(autoRefreshInterval);
|
||||
autoRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting to artifacts array
|
||||
function applySorting(artifacts) {
|
||||
if (!currentSortColumn) return artifacts;
|
||||
|
||||
return artifacts.sort((a, b) => {
|
||||
let aVal = a[currentSortColumn] || '';
|
||||
let bVal = b[currentSortColumn] || '';
|
||||
|
||||
// Handle date sorting
|
||||
if (currentSortColumn === 'created_at') {
|
||||
aVal = new Date(aVal).getTime();
|
||||
bVal = new Date(bVal).getTime();
|
||||
} else {
|
||||
// String comparison (case insensitive)
|
||||
aVal = String(aVal).toLowerCase();
|
||||
bVal = String(bVal).toLowerCase();
|
||||
}
|
||||
|
||||
if (aVal < bVal) return currentSortDirection === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return currentSortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Sorting functionality
|
||||
function sortTable(column) {
|
||||
// Toggle sort direction if clicking same column
|
||||
if (currentSortColumn === column) {
|
||||
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSortColumn = column;
|
||||
currentSortDirection = 'asc';
|
||||
}
|
||||
|
||||
// Update sort indicators
|
||||
document.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
const sortedHeader = event.target.closest('th');
|
||||
sortedHeader.classList.add(`sort-${currentSortDirection}`);
|
||||
|
||||
// Apply filter and sort
|
||||
filterTable();
|
||||
}
|
||||
|
||||
// Filtering functionality - searches across all columns
|
||||
function filterTable() {
|
||||
const searchTerm = document.getElementById('filter-search').value.toLowerCase();
|
||||
|
||||
const filteredArtifacts = allArtifacts.filter(artifact => {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
// Search across all relevant fields
|
||||
const searchableText = [
|
||||
artifact.test_suite || '',
|
||||
artifact.filename || '',
|
||||
artifact.test_name || '',
|
||||
formatDate(artifact.created_at)
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
return searchableText.includes(searchTerm);
|
||||
});
|
||||
|
||||
displayArtifacts(filteredArtifacts);
|
||||
}
|
||||
|
||||
// Clear all filters
|
||||
function clearFilters() {
|
||||
document.getElementById('filter-search').value = '';
|
||||
filterTable();
|
||||
}
|
||||
Reference in New Issue
Block a user