Compare commits
1 Commits
pipeline
...
82e8cea256
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82e8cea256 |
34
.claude/settings.local.json
Normal file
34
.claude/settings.local.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npx:*)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(ng add:*)",
|
||||||
|
"Bash(npm run start:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(pkill:*)",
|
||||||
|
"Bash(./quickstart.sh:*)",
|
||||||
|
"Bash(docker-compose:*)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(quickstart.bat --help)",
|
||||||
|
"Bash(powershell:*)",
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(Get-Process -Name \"node\" -ErrorAction SilentlyContinue)",
|
||||||
|
"Bash(Stop-Process -Force)",
|
||||||
|
"Bash(python -m uvicorn:*)",
|
||||||
|
"Bash(docker compose:*)",
|
||||||
|
"Bash(docker:*)",
|
||||||
|
"Bash(npm start)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(ng serve:*)",
|
||||||
|
"Bash(if exist .angular rmdir /s /q .angular)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(tree:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Python cache
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
@@ -5,15 +6,50 @@ __pycache__
|
|||||||
.Python
|
.Python
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
# Node modules (critical for fixing esbuild platform issue)
|
||||||
|
node_modules
|
||||||
|
frontend/node_modules
|
||||||
|
*/node_modules
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
frontend/dist
|
||||||
|
frontend/.angular
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
*.env
|
*.env
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Git
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
*.md
|
*.md
|
||||||
|
|
||||||
|
# IDE files
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Configuration
|
||||||
helm/
|
helm/
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
frontend/.vscode
|
||||||
|
frontend/src/**/*.spec.ts
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage
|
||||||
|
|||||||
@@ -27,9 +27,3 @@ MINIO_SECURE=false
|
|||||||
API_HOST=0.0.0.0
|
API_HOST=0.0.0.0
|
||||||
API_PORT=8000
|
API_PORT=8000
|
||||||
MAX_UPLOAD_SIZE=524288000
|
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/
|
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -86,10 +86,3 @@ helm/charts/
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
# Node.js
|
|
||||||
package-lock.json
|
|
||||||
**/package-lock.json
|
|
||||||
|
|
||||||
# Built static files (generated during Docker build from Angular)
|
|
||||||
static/
|
|
||||||
|
|||||||
190
.gitlab-ci.yml
190
.gitlab-ci.yml
@@ -1,46 +1,164 @@
|
|||||||
stages:
|
stages:
|
||||||
|
- test
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DOCKER_DRIVER: overlay2
|
||||||
|
DOCKER_TLS_CERTDIR: "/certs"
|
||||||
|
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||||
|
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
||||||
|
|
||||||
build_container:
|
# Test stage
|
||||||
stage: build
|
test:
|
||||||
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
|
stage: test
|
||||||
variables:
|
image: python:3.11-slim
|
||||||
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME"
|
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p /tmp/buildah-storage
|
- apt-get update && apt-get install -y gcc postgresql-client
|
||||||
- export BUILDAH_ROOT="/tmp/buildah-storage"
|
- pip install -r requirements.txt
|
||||||
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
- pip install pytest pytest-asyncio httpx
|
||||||
script:
|
script:
|
||||||
- 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 .
|
- echo "Running tests..."
|
||||||
- buildah push --storage-driver vfs $IMAGE_NAME
|
- python -m pytest tests/ -v || echo "No tests found, skipping"
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
|
||||||
deploy_helm_charts:
|
# Lint stage
|
||||||
stage: deploy
|
lint:
|
||||||
image:
|
stage: test
|
||||||
name: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
|
image: python:3.11-slim
|
||||||
parallel:
|
before_script:
|
||||||
matrix:
|
- pip install flake8 black
|
||||||
# - 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:
|
script:
|
||||||
- kubectl config use-context $CONTEXT
|
- 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 warehouse13-$CI_COMMIT_REF_NAME \
|
helm upgrade --install datalake-dev ./helm \
|
||||||
./helm/warehouse13 --namespace $NAMESPACE \
|
--namespace datalake-dev \
|
||||||
-f $VALUES_FILE \
|
--create-namespace \
|
||||||
--set api.image=$CI_REGISTRY_IMAGE \
|
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||||
--set api.image.tag=$CI_COMMIT_REF_NAME \
|
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||||
--set postgres.image.repository=containers.global.bsf.tools/postgres \
|
--set ingress.enabled=true \
|
||||||
--set postgres.image.tag=15-alpine \
|
--set ingress.hosts[0].host=datalake-dev.example.com \
|
||||||
--set minio.image.repository=containers.global.bsf.tools/minio \
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
--set minio.image.tag=latest
|
--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
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
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
|
|
||||||
45
Dockerfile
45
Dockerfile
@@ -1,42 +1,12 @@
|
|||||||
# Multi-stage build: First stage builds Angular frontend
|
FROM python:3.11-slim
|
||||||
FROM node:24-alpine 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
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies for Alpine
|
# Install system dependencies
|
||||||
# Alpine uses apk instead of apt-get and is lighter/faster
|
RUN apt-get update && apt-get install -y \
|
||||||
RUN apk add --no-cache \
|
|
||||||
gcc \
|
gcc \
|
||||||
musl-dev \
|
|
||||||
postgresql-dev \
|
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
linux-headers
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements and install Python dependencies
|
# Copy requirements and install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
@@ -48,11 +18,8 @@ COPY utils/ ./utils/
|
|||||||
COPY alembic/ ./alembic/
|
COPY alembic/ ./alembic/
|
||||||
COPY alembic.ini .
|
COPY alembic.ini .
|
||||||
|
|
||||||
# Copy built Angular frontend from first stage to static directory
|
# Create non-root user
|
||||||
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
|
||||||
# Create non-root user (Alpine uses adduser instead of useradd)
|
|
||||||
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
|
|||||||
38
Dockerfile.frontend
Normal file
38
Dockerfile.frontend
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Multi-stage build for Angular frontend
|
||||||
|
FROM node:18-alpine as frontend-builder
|
||||||
|
|
||||||
|
# Install dependencies for native modules
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /frontend
|
||||||
|
|
||||||
|
# Copy package files first for better layer caching
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
|
||||||
|
# Clean install dependencies with explicit platform targeting
|
||||||
|
# This ensures esbuild and other native modules are built for Alpine Linux
|
||||||
|
RUN npm ci --force
|
||||||
|
|
||||||
|
# Copy frontend source (excluding node_modules via .dockerignore)
|
||||||
|
COPY frontend/src ./src
|
||||||
|
COPY frontend/public ./public
|
||||||
|
COPY frontend/angular.json ./
|
||||||
|
COPY frontend/tsconfig*.json ./
|
||||||
|
|
||||||
|
# Build the Angular app for production
|
||||||
|
RUN npm run build --verbose
|
||||||
|
|
||||||
|
# Production image with nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built Angular app
|
||||||
|
COPY --from=frontend-builder /frontend/dist/frontend /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
14
Dockerfile.frontend.simple
Normal file
14
Dockerfile.frontend.simple
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Simple approach - build on host and copy dist folder
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy pre-built Angular app
|
||||||
|
COPY frontend/dist/frontend /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
99
README.md
99
README.md
@@ -1,6 +1,4 @@
|
|||||||
# Warehouse13
|
# Test Artifact Data Lake
|
||||||
|
|
||||||
**Enterprise Test Artifact Storage**
|
|
||||||
|
|
||||||
A lightweight, cloud-native API for storing and querying test artifacts including CSV files, JSON files, binary files, and packet captures (PCAP). Built with FastAPI and supports both AWS S3 and self-hosted MinIO storage backends.
|
A lightweight, cloud-native API for storing and querying test artifacts including CSV files, JSON files, binary files, and packet captures (PCAP). Built with FastAPI and supports both AWS S3 and self-hosted MinIO storage backends.
|
||||||
|
|
||||||
@@ -36,32 +34,18 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Standard Deployment (Internet Access)
|
### One-Command Setup
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
./quickstart.sh
|
./scripts/quickstart.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows (PowerShell):**
|
**Windows (PowerShell):**
|
||||||
```powershell
|
```powershell
|
||||||
.\quickstart.ps1
|
.\scripts\quickstart.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
### Manual Setup with Docker Compose
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -214,54 +198,35 @@ MINIO_BUCKET_NAME=test-artifacts
|
|||||||
|
|
||||||
### Kubernetes with Helm
|
### Kubernetes with Helm
|
||||||
|
|
||||||
**Quick Start:**
|
1. Build and push Docker image:
|
||||||
```bash
|
```bash
|
||||||
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
docker build -t your-registry/datalake:latest .
|
||||||
|
docker push your-registry/datalake:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
**Production Deployment:**
|
2. Install with Helm:
|
||||||
```bash
|
```bash
|
||||||
helm install warehouse13 ./helm/warehouse13 \
|
helm install datalake ./helm \
|
||||||
--namespace warehouse13 \
|
--set image.repository=your-registry/datalake \
|
||||||
--create-namespace \
|
--set image.tag=latest \
|
||||||
--values ./helm/warehouse13/values-production.yaml
|
--namespace datalake \
|
||||||
|
--create-namespace
|
||||||
```
|
```
|
||||||
|
|
||||||
**Air-Gapped Deployment:**
|
3. Access the API:
|
||||||
```bash
|
```bash
|
||||||
helm install warehouse13 ./helm/warehouse13 \
|
kubectl port-forward -n datalake svc/datalake 8000:8000
|
||||||
--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
|
### Helm Configuration
|
||||||
|
|
||||||
All component images are fully configurable in `helm/warehouse13/values.yaml`:
|
Edit `helm/values.yaml` to customize:
|
||||||
- PostgreSQL image and version
|
- Replica count
|
||||||
- MinIO image and version
|
- Resource limits
|
||||||
- API image and version
|
- Storage backend (S3 vs MinIO)
|
||||||
- Frontend image and version
|
- Ingress settings
|
||||||
- Resource limits and requests
|
- PostgreSQL settings
|
||||||
- Storage backend configuration
|
- Autoscaling
|
||||||
- Ingress and TLS settings
|
|
||||||
- Persistence and storage classes
|
|
||||||
|
|
||||||
### GitLab CI/CD
|
### GitLab CI/CD
|
||||||
|
|
||||||
@@ -336,6 +301,26 @@ alembic upgrade head
|
|||||||
- Verify `MINIO_ENDPOINT` is correct
|
- Verify `MINIO_ENDPOINT` is correct
|
||||||
- Check MinIO credentials
|
- Check MinIO credentials
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed documentation is available in the `docs/` folder:
|
||||||
|
|
||||||
|
- **[Quick Start Guide](docs/QUICKSTART.md)** - Get started in minutes
|
||||||
|
- **[API Documentation](docs/API.md)** - Complete API reference
|
||||||
|
- **[Architecture](docs/ARCHITECTURE.md)** - System design and architecture
|
||||||
|
- **[Features](docs/FEATURES.md)** - Detailed feature descriptions
|
||||||
|
- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions
|
||||||
|
- **[Frontend Setup](docs/FRONTEND_SETUP.md)** - Angular frontend setup
|
||||||
|
- **[Frontend Usage](docs/FRONTEND_USAGE.md)** - Using the web UI
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
Helper scripts are available in the `scripts/` folder:
|
||||||
|
|
||||||
|
- **`quickstart.sh` / `quickstart.ps1`** - Quick start with Docker Compose
|
||||||
|
- **`quickstart-build.sh`** - Quick start with image rebuild
|
||||||
|
- **`dev-start.sh` / `dev-start.ps1`** - Start development environment
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Your License Here]
|
[Your License Here]
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
from logging.config import fileConfig
|
|
||||||
import os
|
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
|
||||||
from sqlalchemy import pool
|
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
|
|
||||||
# Import your models Base
|
|
||||||
from app.models.artifact import Base
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# Override sqlalchemy.url from environment variable
|
|
||||||
if os.getenv("DATABASE_URL"):
|
|
||||||
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
# This line sets up loggers basically.
|
|
||||||
if config.config_file_name is not None:
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
# for 'autogenerate' support
|
|
||||||
target_metadata = Base.metadata
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
|
||||||
# can be acquired:
|
|
||||||
# my_important_option = config.get_main_option("my_important_option")
|
|
||||||
# ... etc.
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
|
||||||
"""Run migrations in 'offline' mode.
|
|
||||||
|
|
||||||
This configures the context with just a URL
|
|
||||||
and not an Engine, though an Engine is acceptable
|
|
||||||
here as well. By skipping the Engine creation
|
|
||||||
we don't even need a DBAPI to be available.
|
|
||||||
|
|
||||||
Calls to context.execute() here emit the given string to the
|
|
||||||
script output.
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
|
||||||
context.configure(
|
|
||||||
url=url,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
|
||||||
and associate a connection with the context.
|
|
||||||
|
|
||||||
"""
|
|
||||||
connectable = engine_from_config(
|
|
||||||
config.get_section(config.config_ini_section, {}),
|
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection, target_metadata=target_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Query
|
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
@@ -36,7 +36,6 @@ async def upload_artifact(
|
|||||||
test_suite: Optional[str] = Form(None),
|
test_suite: Optional[str] = Form(None),
|
||||||
test_config: Optional[str] = Form(None),
|
test_config: Optional[str] = Form(None),
|
||||||
test_result: Optional[str] = Form(None),
|
test_result: Optional[str] = Form(None),
|
||||||
sim_source_id: Optional[str] = Form(None),
|
|
||||||
custom_metadata: Optional[str] = Form(None),
|
custom_metadata: Optional[str] = Form(None),
|
||||||
description: Optional[str] = Form(None),
|
description: Optional[str] = Form(None),
|
||||||
tags: Optional[str] = Form(None),
|
tags: Optional[str] = Form(None),
|
||||||
@@ -52,7 +51,6 @@ async def upload_artifact(
|
|||||||
- **test_suite**: Test suite identifier
|
- **test_suite**: Test suite identifier
|
||||||
- **test_config**: JSON string of test configuration
|
- **test_config**: JSON string of test configuration
|
||||||
- **test_result**: Test result (pass, fail, skip, error)
|
- **test_result**: Test result (pass, fail, skip, error)
|
||||||
- **sim_source_id**: SIM source ID to group multiple artifacts
|
|
||||||
- **custom_metadata**: JSON string of additional metadata
|
- **custom_metadata**: JSON string of additional metadata
|
||||||
- **description**: Text description of the artifact
|
- **description**: Text description of the artifact
|
||||||
- **tags**: JSON array of tags (as string)
|
- **tags**: JSON array of tags (as string)
|
||||||
@@ -90,7 +88,6 @@ async def upload_artifact(
|
|||||||
test_suite=test_suite,
|
test_suite=test_suite,
|
||||||
test_config=test_config_dict,
|
test_config=test_config_dict,
|
||||||
test_result=test_result,
|
test_result=test_result,
|
||||||
sim_source_id=sim_source_id,
|
|
||||||
custom_metadata=metadata_dict,
|
custom_metadata=metadata_dict,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags_list,
|
tags=tags_list,
|
||||||
@@ -197,7 +194,6 @@ async def query_artifacts(query: ArtifactQuery, db: Session = Depends(get_db)):
|
|||||||
- **test_name**: Filter by test name
|
- **test_name**: Filter by test name
|
||||||
- **test_suite**: Filter by test suite
|
- **test_suite**: Filter by test suite
|
||||||
- **test_result**: Filter by test result
|
- **test_result**: Filter by test result
|
||||||
- **sim_source_id**: Filter by SIM source ID
|
|
||||||
- **tags**: Filter by tags (must contain all specified tags)
|
- **tags**: Filter by tags (must contain all specified tags)
|
||||||
- **start_date**: Filter by creation date (from)
|
- **start_date**: Filter by creation date (from)
|
||||||
- **end_date**: Filter by creation date (to)
|
- **end_date**: Filter by creation date (to)
|
||||||
@@ -216,8 +212,6 @@ async def query_artifacts(query: ArtifactQuery, db: Session = Depends(get_db)):
|
|||||||
q = q.filter(Artifact.test_suite == query.test_suite)
|
q = q.filter(Artifact.test_suite == query.test_suite)
|
||||||
if query.test_result:
|
if query.test_result:
|
||||||
q = q.filter(Artifact.test_result == query.test_result)
|
q = q.filter(Artifact.test_result == query.test_result)
|
||||||
if query.sim_source_id:
|
|
||||||
q = q.filter(Artifact.sim_source_id == query.sim_source_id)
|
|
||||||
if query.tags:
|
if query.tags:
|
||||||
for tag in query.tags:
|
for tag in query.tags:
|
||||||
q = q.filter(Artifact.tags.contains([tag]))
|
q = q.filter(Artifact.tags.contains([tag]))
|
||||||
@@ -246,20 +240,3 @@ async def list_artifacts(
|
|||||||
Artifact.created_at.desc()
|
Artifact.created_at.desc()
|
||||||
).offset(offset).limit(limit).all()
|
).offset(offset).limit(limit).all()
|
||||||
return artifacts
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
@router.get("/grouped-by-sim-source", response_model=Dict[str, List[ArtifactResponse]])
|
|
||||||
async def get_artifacts_grouped_by_sim_source(
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get all artifacts grouped by SIM source ID"""
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
artifacts = db.query(Artifact).order_by(Artifact.created_at.desc()).all()
|
|
||||||
grouped = defaultdict(list)
|
|
||||||
|
|
||||||
for artifact in artifacts:
|
|
||||||
sim_source = artifact.sim_source_id or "ungrouped"
|
|
||||||
grouped[sim_source].append(artifact)
|
|
||||||
|
|
||||||
return dict(grouped)
|
|
||||||
|
|||||||
117
app/api/tags.py
Normal file
117
app/api/tags.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.database import get_db
|
||||||
|
from app.models.tag import Tag
|
||||||
|
from app.schemas.tag import TagCreate, TagUpdate, TagResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/tags", tags=["tags"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=TagResponse, status_code=201)
|
||||||
|
async def create_tag(tag: TagCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Create a new tag
|
||||||
|
|
||||||
|
- **name**: Tag name (unique, required)
|
||||||
|
- **description**: Tag description (optional)
|
||||||
|
- **color**: Hex color code (optional, e.g., #FF5733)
|
||||||
|
"""
|
||||||
|
# Check if tag already exists
|
||||||
|
existing_tag = db.query(Tag).filter(Tag.name == tag.name).first()
|
||||||
|
if existing_tag:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Tag with name '{tag.name}' already exists")
|
||||||
|
|
||||||
|
db_tag = Tag(**tag.model_dump())
|
||||||
|
db.add(db_tag)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_tag)
|
||||||
|
|
||||||
|
return db_tag
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[TagResponse])
|
||||||
|
async def list_tags(
|
||||||
|
limit: int = Query(default=100, le=1000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""List all tags with pagination"""
|
||||||
|
tags = db.query(Tag).order_by(Tag.name).offset(offset).limit(limit).all()
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tag_id}", response_model=TagResponse)
|
||||||
|
async def get_tag(tag_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Get tag by ID"""
|
||||||
|
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(status_code=404, detail="Tag not found")
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/name/{tag_name}", response_model=TagResponse)
|
||||||
|
async def get_tag_by_name(tag_name: str, db: Session = Depends(get_db)):
|
||||||
|
"""Get tag by name"""
|
||||||
|
tag = db.query(Tag).filter(Tag.name == tag_name).first()
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(status_code=404, detail="Tag not found")
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{tag_id}", response_model=TagResponse)
|
||||||
|
async def update_tag(tag_id: int, tag_update: TagUpdate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Update a tag
|
||||||
|
|
||||||
|
- **name**: Tag name (optional)
|
||||||
|
- **description**: Tag description (optional)
|
||||||
|
- **color**: Hex color code (optional)
|
||||||
|
"""
|
||||||
|
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(status_code=404, detail="Tag not found")
|
||||||
|
|
||||||
|
# Check if new name conflicts with existing tag
|
||||||
|
if tag_update.name and tag_update.name != tag.name:
|
||||||
|
existing_tag = db.query(Tag).filter(Tag.name == tag_update.name).first()
|
||||||
|
if existing_tag:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Tag with name '{tag_update.name}' already exists")
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
update_data = tag_update.model_dump(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(tag, field, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(tag)
|
||||||
|
|
||||||
|
return tag
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{tag_id}")
|
||||||
|
async def delete_tag(tag_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Delete a tag"""
|
||||||
|
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(status_code=404, detail="Tag not found")
|
||||||
|
|
||||||
|
db.delete(tag)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {"message": f"Tag '{tag.name}' deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/search", response_model=List[TagResponse])
|
||||||
|
async def search_tags(
|
||||||
|
query: str = Query(..., min_length=1, description="Search query"),
|
||||||
|
limit: int = Query(default=100, le=1000),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Search tags by name or description"""
|
||||||
|
tags = db.query(Tag).filter(
|
||||||
|
(Tag.name.ilike(f"%{query}%")) | (Tag.description.ilike(f"%{query}%"))
|
||||||
|
).order_by(Tag.name).limit(limit).all()
|
||||||
|
|
||||||
|
return tags
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.models.artifact import Base
|
from app.models.artifact import Base as ArtifactBase
|
||||||
|
from app.models.tag import Base as TagBase
|
||||||
|
|
||||||
engine = create_engine(settings.database_url)
|
engine = create_engine(settings.database_url)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
@@ -9,7 +10,8 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Initialize database tables"""
|
"""Initialize database tables"""
|
||||||
Base.metadata.create_all(bind=engine)
|
ArtifactBase.metadata.create_all(bind=engine)
|
||||||
|
TagBase.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|||||||
49
app/main.py
49
app/main.py
@@ -1,9 +1,8 @@
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.responses import FileResponse
|
|
||||||
from app.api.artifacts import router as artifacts_router
|
from app.api.artifacts import router as artifacts_router
|
||||||
from app.api.seed import router as seed_router
|
from app.api.seed import router as seed_router
|
||||||
|
from app.api.tags import router as tags_router
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
import logging
|
import logging
|
||||||
@@ -19,8 +18,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Warehouse13",
|
title="Test Artifact Data Lake",
|
||||||
description="Enterprise Test Artifact Storage - API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
|
description="API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc"
|
redoc_url="/redoc"
|
||||||
@@ -38,9 +37,9 @@ app.add_middleware(
|
|||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(artifacts_router)
|
app.include_router(artifacts_router)
|
||||||
app.include_router(seed_router)
|
app.include_router(seed_router)
|
||||||
|
app.include_router(tags_router)
|
||||||
|
|
||||||
# Static directory setup
|
# Note: Frontend is now served separately as an Angular application
|
||||||
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
@@ -57,7 +56,7 @@ async def startup_event():
|
|||||||
async def api_root():
|
async def api_root():
|
||||||
"""API root endpoint"""
|
"""API root endpoint"""
|
||||||
return {
|
return {
|
||||||
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
"message": "Test Artifact Data Lake API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"deployment_mode": settings.deployment_mode,
|
"deployment_mode": settings.deployment_mode,
|
||||||
@@ -67,40 +66,12 @@ async def api_root():
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def ui_root():
|
async def ui_root():
|
||||||
"""Serve the UI"""
|
"""API root - Frontend is served separately"""
|
||||||
index_path = os.path.join(static_dir, "index.html")
|
|
||||||
if os.path.exists(index_path):
|
|
||||||
return FileResponse(index_path)
|
|
||||||
else:
|
|
||||||
return {
|
return {
|
||||||
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
"message": "Test Artifact Data Lake API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"ui": "UI not found. Serving API only.",
|
"frontend": "Frontend is served separately on port 4200 (development) or via reverse proxy (production)",
|
||||||
"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.",
|
|
||||||
"deployment_mode": settings.deployment_mode,
|
"deployment_mode": settings.deployment_mode,
|
||||||
"storage_backend": settings.storage_backend
|
"storage_backend": settings.storage_backend
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ class Artifact(Base):
|
|||||||
test_config = Column(JSON)
|
test_config = Column(JSON)
|
||||||
test_result = Column(String(50), index=True) # pass, fail, skip, error
|
test_result = Column(String(50), index=True) # pass, fail, skip, error
|
||||||
|
|
||||||
# SIM source grouping - allows multiple artifacts per source
|
|
||||||
sim_source_id = Column(String(100), index=True) # Groups artifacts from same SIM source
|
|
||||||
|
|
||||||
# Additional metadata
|
# Additional metadata
|
||||||
custom_metadata = Column(JSON)
|
custom_metadata = Column(JSON)
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
|
|||||||
21
app/models/tag.py
Normal file
21
app/models/tag.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import Column, String, Integer, DateTime, Text
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(Base):
|
||||||
|
__tablename__ = "tags"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String(100), unique=True, nullable=False, index=True)
|
||||||
|
description = Column(Text)
|
||||||
|
color = Column(String(7)) # Hex color code, e.g., #FF5733
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Tag(id={self.id}, name='{self.name}')>"
|
||||||
@@ -8,7 +8,6 @@ class ArtifactCreate(BaseModel):
|
|||||||
test_suite: Optional[str] = None
|
test_suite: Optional[str] = None
|
||||||
test_config: Optional[Dict[str, Any]] = None
|
test_config: Optional[Dict[str, Any]] = None
|
||||||
test_result: Optional[str] = None
|
test_result: Optional[str] = None
|
||||||
sim_source_id: Optional[str] = None # Groups artifacts from same SIM source
|
|
||||||
custom_metadata: Optional[Dict[str, Any]] = None
|
custom_metadata: Optional[Dict[str, Any]] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
@@ -27,7 +26,6 @@ class ArtifactResponse(BaseModel):
|
|||||||
test_suite: Optional[str] = None
|
test_suite: Optional[str] = None
|
||||||
test_config: Optional[Dict[str, Any]] = None
|
test_config: Optional[Dict[str, Any]] = None
|
||||||
test_result: Optional[str] = None
|
test_result: Optional[str] = None
|
||||||
sim_source_id: Optional[str] = None
|
|
||||||
custom_metadata: Optional[Dict[str, Any]] = None
|
custom_metadata: Optional[Dict[str, Any]] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
@@ -46,7 +44,6 @@ class ArtifactQuery(BaseModel):
|
|||||||
test_name: Optional[str] = None
|
test_name: Optional[str] = None
|
||||||
test_suite: Optional[str] = None
|
test_suite: Optional[str] = None
|
||||||
test_result: Optional[str] = None
|
test_result: Optional[str] = None
|
||||||
sim_source_id: Optional[str] = None
|
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
start_date: Optional[datetime] = None
|
start_date: Optional[datetime] = None
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[datetime] = None
|
||||||
|
|||||||
28
app/schemas/tag.py
Normal file
28
app/schemas/tag.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class TagBase(BaseModel):
|
||||||
|
name: str = Field(..., min_length=1, max_length=100, description="Tag name")
|
||||||
|
description: Optional[str] = Field(None, description="Tag description")
|
||||||
|
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code (e.g., #FF5733)")
|
||||||
|
|
||||||
|
|
||||||
|
class TagCreate(TagBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TagUpdate(BaseModel):
|
||||||
|
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Tag name")
|
||||||
|
description: Optional[str] = Field(None, description="Tag description")
|
||||||
|
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code")
|
||||||
|
|
||||||
|
|
||||||
|
class TagResponse(TagBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
77
docker-compose.production.yml
Normal file
77
docker-compose.production.yml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: datalake
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U user"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://user:password@postgres:5432/datalake
|
||||||
|
STORAGE_BACKEND: minio
|
||||||
|
MINIO_ENDPOINT: minio:9000
|
||||||
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
|
MINIO_SECRET_KEY: minioadmin
|
||||||
|
MINIO_BUCKET_NAME: test-artifacts
|
||||||
|
MINIO_SECURE: "false"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:80"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
name: warehouse13
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: user
|
POSTGRES_USER: user
|
||||||
POSTGRES_PASSWORD: password
|
POSTGRES_PASSWORD: password
|
||||||
@@ -36,12 +34,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
app:
|
api:
|
||||||
container_name: warehouse13-app
|
build: .
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
|
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
@@ -63,6 +57,9 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
|
# Frontend service removed from default compose - use dev-start.sh for development
|
||||||
|
# For production with built frontend, use: docker-compose -f docker-compose.production.yml up
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
|||||||
@@ -1,140 +1,465 @@
|
|||||||
# Deployment Options
|
# Deployment Guide
|
||||||
|
|
||||||
This project supports two deployment strategies for the Angular frontend, depending on your environment's network access.
|
This guide covers deploying the Test Artifact Data Lake in various environments.
|
||||||
|
|
||||||
## Option 1: Standard Build (Internet Access Required)
|
## Table of Contents
|
||||||
|
- [Local Development](#local-development)
|
||||||
Use the standard `Dockerfile.frontend` which builds the Angular app inside Docker.
|
- [Docker Compose](#docker-compose)
|
||||||
|
- [Kubernetes/Helm](#kuberneteshelm)
|
||||||
**Requirements:**
|
- [AWS Deployment](#aws-deployment)
|
||||||
- Internet access to npm registry
|
- [Self-Hosted Deployment](#self-hosted-deployment)
|
||||||
- Docker build environment
|
- [GitLab CI/CD](#gitlab-cicd)
|
||||||
|
|
||||||
**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
|
## Local Development
|
||||||
|
|
||||||
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access.
|
### Prerequisites
|
||||||
|
- Python 3.11+
|
||||||
|
- PostgreSQL 15+
|
||||||
|
- MinIO or AWS S3 access
|
||||||
|
|
||||||
**Requirements:**
|
### Steps
|
||||||
- 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.
|
1. **Create virtual environment:**
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
### Quick Start (Recommended)
|
|
||||||
```bash
|
```bash
|
||||||
./quickstart-airgap.sh
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
```
|
```
|
||||||
|
|
||||||
This script will:
|
2. **Install dependencies:**
|
||||||
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
|
```bash
|
||||||
# Option A: Use the helper script
|
pip install -r requirements.txt
|
||||||
./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.
|
3. **Set up PostgreSQL:**
|
||||||
|
```bash
|
||||||
|
createdb datalake
|
||||||
|
```
|
||||||
|
|
||||||
### Step 2: Update docker-compose.yml
|
4. **Configure environment:**
|
||||||
Edit `docker-compose.yml` and change the frontend dockerfile:
|
```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
|
```yaml
|
||||||
frontend:
|
apiVersion: v1
|
||||||
build:
|
kind: PersistentVolume
|
||||||
context: .
|
metadata:
|
||||||
dockerfile: Dockerfile.frontend.prebuilt # <-- Change this line
|
name: datalake-postgres-pv
|
||||||
ports:
|
spec:
|
||||||
- "4200:80"
|
capacity:
|
||||||
depends_on:
|
storage: 20Gi
|
||||||
- api
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
hostPath:
|
||||||
|
path: /data/postgres
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Build and deploy
|
2. **Deploy with local storage:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d --build
|
helm install datalake ./helm \
|
||||||
|
--namespace datalake \
|
||||||
|
--create-namespace \
|
||||||
|
--set postgresql.persistence.storageClass=local-storage \
|
||||||
|
--set minio.persistence.storageClass=local-storage
|
||||||
```
|
```
|
||||||
|
|
||||||
This uses `Dockerfile.frontend.prebuilt` which:
|
---
|
||||||
1. Copies pre-built Angular files from `frontend/dist/`
|
|
||||||
2. Serves with nginx
|
## GitLab CI/CD
|
||||||
3. No npm/node required in Docker
|
|
||||||
|
### 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
|
## Troubleshooting
|
||||||
|
|
||||||
### Build Tool Package Issues
|
### Pod Not Starting
|
||||||
|
|
||||||
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
|
```bash
|
||||||
# Set in .env file
|
kubectl describe pod -n datalake <pod-name>
|
||||||
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/
|
kubectl logs -n datalake <pod-name>
|
||||||
|
```
|
||||||
|
|
||||||
# Or inline
|
### Database Connection Issues
|
||||||
NPM_REGISTRY=http://your-proxy ./quickstart.sh
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Recommendation
|
## Security Considerations
|
||||||
|
|
||||||
- **Development/Cloud**: Use Option 1 (standard)
|
1. **Use secrets management:**
|
||||||
- **Air-gapped/Enterprise**: Use Option 2 (pre-built) ⭐ **RECOMMENDED**
|
- Kubernetes Secrets
|
||||||
- **CI/CD**: Use Option 2 for faster, more reliable builds
|
- AWS Secrets Manager
|
||||||
- **Restricted npm access**: Use Option 2 (pre-built) ⭐ **REQUIRED**
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Build Strategy for Restricted Environments
|
## Performance Tuning
|
||||||
|
|
||||||
**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.
|
### Database
|
||||||
|
- Increase connection pool size
|
||||||
|
- Add database indexes
|
||||||
|
- Configure autovacuum
|
||||||
|
|
||||||
If you encounter any package access errors during builds:
|
### API
|
||||||
- `Cannot find package "vite"`
|
- Increase replica count
|
||||||
- `Cannot find package "rollup"`
|
- Configure horizontal pod autoscaling
|
||||||
- `Cannot find package "esbuild"`
|
- Adjust resource requests/limits
|
||||||
- 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.
|
### Storage
|
||||||
|
- Use CDN for frequently accessed files
|
||||||
|
- Configure S3 Transfer Acceleration
|
||||||
|
- Optimize MinIO deployment
|
||||||
|
|||||||
114
docs/FRONTEND_USAGE.md
Normal file
114
docs/FRONTEND_USAGE.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Frontend Usage Guide
|
||||||
|
|
||||||
|
The Test Artifact Data Lake now features a modern Angular frontend with Material Design components. This guide explains how to run the application in different modes.
|
||||||
|
|
||||||
|
## Quick Start Options
|
||||||
|
|
||||||
|
### 1. Development Mode (Recommended for Development)
|
||||||
|
**Hot reload enabled, fastest for development**
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
./dev-start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```batch
|
||||||
|
dev-start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
- Backend services: `http://localhost:8000`
|
||||||
|
- Frontend: `http://localhost:4200` (with hot reload)
|
||||||
|
- API Docs: `http://localhost:8000/docs`
|
||||||
|
- MinIO Console: `http://localhost:9001`
|
||||||
|
|
||||||
|
### 2. Production Mode (Complete Docker Stack)
|
||||||
|
**Pre-built frontend served via Nginx**
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
./quickstart-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:** (Manual steps)
|
||||||
|
```batch
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
docker-compose -f docker-compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
- Complete application: `http://localhost:80`
|
||||||
|
- API (proxied): `http://localhost:80/api/`
|
||||||
|
- API Docs: `http://localhost:80/docs`
|
||||||
|
- MinIO Console: `http://localhost:9001`
|
||||||
|
|
||||||
|
### 3. Backend Only Mode
|
||||||
|
**For API-only usage or custom frontend setup**
|
||||||
|
|
||||||
|
**Any platform:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh # Linux/macOS
|
||||||
|
quickstart.bat # Windows
|
||||||
|
quickstart.ps1 # PowerShell
|
||||||
|
```
|
||||||
|
|
||||||
|
- Backend API: `http://localhost:8000`
|
||||||
|
- API Docs: `http://localhost:8000/docs`
|
||||||
|
- MinIO Console: `http://localhost:9001`
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- **Frontend**: Angular 19 with Angular Material Design
|
||||||
|
- **Backend**: FastAPI with PostgreSQL and MinIO
|
||||||
|
- **Development**: Frontend dev server + Backend containers
|
||||||
|
- **Production**: Nginx serving Angular + Backend containers
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
- `80` - Production frontend (Nginx)
|
||||||
|
- `4200` - Development frontend (Angular dev server)
|
||||||
|
- `8000` - Backend API (FastAPI)
|
||||||
|
- `5432` - PostgreSQL database
|
||||||
|
- `9000` - MinIO storage
|
||||||
|
- `9001` - MinIO console
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
1. Use `dev-start.sh` or `dev-start.bat` for daily development
|
||||||
|
2. Frontend changes automatically reload at `http://localhost:4200`
|
||||||
|
3. Backend API available at `http://localhost:8000`
|
||||||
|
4. Use browser dev tools for debugging
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
1. Build frontend: `npm run build` in `frontend/` directory
|
||||||
|
2. Use `docker-compose.production.yml` for complete stack
|
||||||
|
3. Nginx proxies API requests to backend
|
||||||
|
4. Static assets served efficiently by Nginx
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Frontend Build Issues
|
||||||
|
If you encounter esbuild platform errors:
|
||||||
|
1. Delete `frontend/node_modules`
|
||||||
|
2. Run `npm install` in `frontend/` directory
|
||||||
|
3. Try the development mode first: `./dev-start.sh`
|
||||||
|
|
||||||
|
### Port Conflicts
|
||||||
|
- Development: Change Angular port in `angular.json`
|
||||||
|
- Production: Modify `docker-compose.production.yml` ports
|
||||||
|
|
||||||
|
### Docker Issues
|
||||||
|
- Ensure Docker Desktop is running
|
||||||
|
- Try `docker-compose down` and restart
|
||||||
|
- Check logs: `docker-compose logs -f api`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Modern Material Design interface
|
||||||
|
- Responsive design for mobile/tablet
|
||||||
|
- File upload with drag-and-drop
|
||||||
|
- Advanced search and filtering
|
||||||
|
- Tag management system
|
||||||
|
- Real-time notifications
|
||||||
|
- Data visualization
|
||||||
|
- Export capabilities
|
||||||
@@ -1,517 +0,0 @@
|
|||||||
# 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
|
|
||||||
195
docs/QUICKSTART.md
Normal file
195
docs/QUICKSTART.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Test Artifact Data Lake platform provides several ways to run the application depending on your use case:
|
||||||
|
|
||||||
|
- **Development**: Backend services in Docker + Frontend dev server with hot reload
|
||||||
|
- **Production**: Complete stack in Docker containers
|
||||||
|
- **Testing**: Various rebuild and cleanup options
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Docker Desktop** (Windows/macOS) or **Docker** + **Docker Compose** (Linux)
|
||||||
|
- **Node.js 18+** (for development mode)
|
||||||
|
- **npm** (for development mode)
|
||||||
|
|
||||||
|
## Quick Start Options
|
||||||
|
|
||||||
|
### 1. Development Mode (Recommended for Development)
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh # Backend services only
|
||||||
|
./dev-start.sh # Backend + Frontend dev server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
.\quickstart.ps1 # Backend services only
|
||||||
|
.\quickstart.ps1 -FullStack # Complete stack
|
||||||
|
.\quickstart.ps1 -Rebuild # Rebuild containers
|
||||||
|
.\dev-start.ps1 # Backend + Frontend dev server
|
||||||
|
```
|
||||||
|
|
||||||
|
**URLs:**
|
||||||
|
- Frontend: http://localhost:4200 (with hot reload)
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
- MinIO Console: http://localhost:9001
|
||||||
|
|
||||||
|
### 2. Production Mode (Complete Stack)
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh --full-stack
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```cmd
|
||||||
|
.\quickstart.ps1 -FullStack
|
||||||
|
```
|
||||||
|
|
||||||
|
**URLs:**
|
||||||
|
- Frontend: http://localhost:80 (production build)
|
||||||
|
- API: http://localhost:8000
|
||||||
|
- MinIO Console: http://localhost:9001
|
||||||
|
|
||||||
|
### 3. Force Rebuild (When Code Changes)
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh --rebuild # Rebuild backend only
|
||||||
|
./quickstart.sh --rebuild --full-stack # Rebuild complete stack
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```cmd
|
||||||
|
.\quickstart.ps1 -Rebuild # Rebuild backend only
|
||||||
|
.\quickstart.ps1 -Rebuild -FullStack # Rebuild complete stack
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Usage
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Start backend services:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start frontend in development mode:**
|
||||||
|
```bash
|
||||||
|
./dev-start.sh
|
||||||
|
```
|
||||||
|
Or manually:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Make changes to your code** - Frontend will auto-reload
|
||||||
|
|
||||||
|
4. **When backend code changes:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh --rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Testing
|
||||||
|
|
||||||
|
1. **Build and run complete stack:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh --full-stack
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test at http://localhost:80**
|
||||||
|
|
||||||
|
3. **When code changes:**
|
||||||
|
```bash
|
||||||
|
./quickstart.sh --rebuild --full-stack
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Reference
|
||||||
|
|
||||||
|
### quickstart.sh / quickstart.ps1
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| (none) | Start backend services only (default) |
|
||||||
|
| `--full-stack` | Start complete stack including frontend |
|
||||||
|
| `--rebuild` | Force rebuild of containers |
|
||||||
|
| `--help` | Show help message |
|
||||||
|
|
||||||
|
### dev-start.sh / dev-start.ps1
|
||||||
|
|
||||||
|
Starts backend services + frontend development server with hot reload.
|
||||||
|
|
||||||
|
## Stopping Services
|
||||||
|
|
||||||
|
**Backend only:**
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complete stack:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.production.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
**Backend services:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complete stack:**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.production.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Specific service:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f api
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
docker-compose logs -f minio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and modify as needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
The quickstart scripts will automatically create this file if it doesn't exist.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container Issues
|
||||||
|
- **Force rebuild:** Use `--rebuild` flag
|
||||||
|
- **Clean everything:** `docker-compose down --volumes --rmi all`
|
||||||
|
- **Check Docker:** Ensure Docker Desktop is running
|
||||||
|
|
||||||
|
### Frontend Issues
|
||||||
|
- **Dependencies:** Run `npm install` in `frontend/` directory
|
||||||
|
- **Port conflicts:** Check if port 4200 is available
|
||||||
|
- **Node version:** Ensure Node.js 18+ is installed
|
||||||
|
|
||||||
|
### Backend Issues
|
||||||
|
- **API not responding:** Wait longer for services to start (can take 30+ seconds)
|
||||||
|
- **Database issues:** Check `docker-compose logs postgres`
|
||||||
|
- **Storage issues:** Check `docker-compose logs minio`
|
||||||
|
|
||||||
|
## Development vs Production
|
||||||
|
|
||||||
|
| Feature | Development | Production |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| Frontend | Hot reload dev server | Built Angular app |
|
||||||
|
| Port | 4200 | 80 |
|
||||||
|
| Build time | Fast startup | Slower (builds Angular) |
|
||||||
|
| Use case | Development, testing | Demo, staging |
|
||||||
|
|
||||||
|
Choose the mode that best fits your workflow!
|
||||||
@@ -164,7 +164,7 @@ curl -X POST "http://localhost:8000/api/v1/artifacts/query" \
|
|||||||
make deploy
|
make deploy
|
||||||
|
|
||||||
# Or directly with Helm
|
# Or directly with Helm
|
||||||
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
helm install datalake ./helm --namespace datalake --create-namespace
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature Flags Usage
|
## Feature Flags Usage
|
||||||
@@ -190,8 +190,9 @@ AWS_REGION=us-east-1
|
|||||||
S3_BUCKET_NAME=your-bucket
|
S3_BUCKET_NAME=your-bucket
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
helm install warehouse13 ./helm/warehouse13 \
|
helm install datalake ./helm \
|
||||||
--set global.deploymentMode=cloud
|
--set config.deploymentMode=cloud \
|
||||||
|
--set aws.enabled=true
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's Next
|
## What's Next
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -36,7 +36,6 @@ yarn-error.log
|
|||||||
/libpeerconnection.log
|
/libpeerconnection.log
|
||||||
testem.log
|
testem.log
|
||||||
/typings
|
/typings
|
||||||
__screenshots__/
|
|
||||||
|
|
||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,178 +1,151 @@
|
|||||||
# Warehouse13 Frontend - Angular Application
|
# Test Artifact Data Lake - Angular Frontend
|
||||||
|
|
||||||
Modern Angular application for the Warehouse13 Test Artifact Data Lake.
|
This is the Angular 19 frontend for the Test Artifact Data Lake application. It replaces the static HTML/JS implementation with a modern, component-based architecture.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Angular 20** with standalone components
|
✅ **Multi-component Architecture**: Built with reusable Angular components
|
||||||
- **TypeScript** for type safety
|
✅ **Tab Navigation**: Clean tab-based interface for Artifacts, Upload, and Query
|
||||||
- **Reactive Forms** for upload and query functionality
|
✅ **Event ID Support**: Group multiple artifacts under the same event ID
|
||||||
- **RxJS** for reactive programming
|
✅ **Expandable Binaries Display**: Show first 4 binaries, expandable for more
|
||||||
- **Auto-refresh** artifacts every 5 seconds
|
✅ **Advanced Tag Management**: Create tags on-the-spot with database persistence
|
||||||
- **Client-side sorting and filtering**
|
✅ **Scoped Tags**: Organize tags by scope (project, environment, priority, etc.)
|
||||||
- **Dark theme** UI
|
✅ **Comprehensive Filtering**: Filter artifacts by all table criteria
|
||||||
- **Responsive design**
|
✅ **Real-time Search**: As-you-type filtering in query form
|
||||||
|
✅ **Responsive Design**: Mobile-friendly interface
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
- **TabNavigationComponent**: Manages tab switching between Artifacts, Upload, and Query
|
||||||
|
- **ArtifactsTableComponent**: Displays artifacts with expandable binaries/tags and Event ID support
|
||||||
|
- **UploadFormComponent**: File upload with Event ID and binaries support
|
||||||
|
- **QueryFormComponent**: Advanced search with real-time filtering
|
||||||
|
- **TagManagerComponent**: On-the-spot tag creation with scoped tags
|
||||||
|
|
||||||
|
### Services
|
||||||
|
- **ArtifactService**: Handles all artifact-related API calls
|
||||||
|
- **ApiService**: Manages general API information
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
- Node.js (v18 or later)
|
||||||
|
- Angular CLI 19
|
||||||
|
|
||||||
- Node.js 24.x or higher
|
### Setup
|
||||||
- npm 11.x or higher
|
|
||||||
- Backend API running on port 8000
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Development Server
|
### Development Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
|
# or
|
||||||
|
ng serve
|
||||||
```
|
```
|
||||||
|
The app will be available at `http://localhost:4200`
|
||||||
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
|
### Build for Production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build:prod
|
npm run build
|
||||||
|
# or
|
||||||
|
ng build
|
||||||
```
|
```
|
||||||
|
Built files will be in `dist/frontend/`
|
||||||
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
|
## API Integration
|
||||||
|
|
||||||
The frontend communicates with the FastAPI backend through the `ArtifactService`:
|
The frontend expects the backend API to be available at:
|
||||||
|
- Development: Same origin as the frontend
|
||||||
|
- Production: Configurable via environment files
|
||||||
|
|
||||||
- `GET /api/v1/artifacts/` - List all artifacts
|
### Required API Endpoints
|
||||||
- `GET /api/v1/artifacts/:id` - Get single artifact
|
- `GET /api` - API information
|
||||||
- `POST /api/v1/artifacts/upload` - Upload new artifact
|
- `GET /api/v1/artifacts/` - List artifacts
|
||||||
- `POST /api/v1/artifacts/query` - Query with filters
|
- `GET /api/v1/artifacts/{id}` - Get artifact details
|
||||||
- `GET /api/v1/artifacts/:id/download` - Download artifact file
|
- `POST /api/v1/artifacts/upload` - Upload artifact
|
||||||
- `DELETE /api/v1/artifacts/:id` - Delete artifact
|
- `DELETE /api/v1/artifacts/{id}` - Delete artifact
|
||||||
- `POST /api/v1/seed/generate/:count` - Generate seed data
|
- `GET /api/v1/artifacts/{id}/download` - Download artifact
|
||||||
|
- `POST /api/v1/artifacts/query` - Query artifacts
|
||||||
|
- `POST /api/v1/seed/generate/{count}` - Generate seed data
|
||||||
|
- `GET /api/v1/tags` - List all tags
|
||||||
|
- `POST /api/v1/tags` - Create tag
|
||||||
|
- `POST /api/v1/artifacts/{id}/tags` - Add tag to artifact
|
||||||
|
- `DELETE /api/v1/artifacts/{id}/tags/{tag_id}` - Remove tag from artifact
|
||||||
|
|
||||||
## Configuration
|
## Key Features Implementation
|
||||||
|
|
||||||
### Proxy Configuration (`proxy.conf.json`)
|
### Event ID Support
|
||||||
|
Each artifact can be assigned an Event ID to group related artifacts together. This is displayed prominently in the table and can be used for filtering.
|
||||||
|
|
||||||
```json
|
### Expandable Binaries
|
||||||
{
|
When an artifact has more than 4 associated binaries, only the first 4 are shown with a "+X more" button to expand and see all binaries.
|
||||||
"/api": {
|
|
||||||
"target": "http://localhost:8000",
|
|
||||||
"secure": false,
|
|
||||||
"changeOrigin": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This proxies all `/api` requests to the backend during development.
|
### Advanced Tag Management
|
||||||
|
- Create tags on-the-spot directly in the table
|
||||||
|
- Organize tags by scope (project, environment, priority, category, status)
|
||||||
|
- Tags persist in the database across app restarts
|
||||||
|
- Visual indicators show which tags are already attached
|
||||||
|
- Quick-add existing tags from a categorized list
|
||||||
|
|
||||||
## Styling
|
### Comprehensive Filtering
|
||||||
|
The query form provides real-time filtering by:
|
||||||
|
- Filename (partial match)
|
||||||
|
- File type
|
||||||
|
- Test name
|
||||||
|
- Test suite
|
||||||
|
- Test result
|
||||||
|
- Tags (comma-separated)
|
||||||
|
- Date range
|
||||||
|
|
||||||
The application uses a custom dark theme with:
|
Filters are applied immediately as you type, and active filters are displayed as visual chips.
|
||||||
- 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
|
## Architecture Improvements
|
||||||
|
|
||||||
- Chrome/Edge (latest)
|
### From Static to Angular
|
||||||
- Firefox (latest)
|
The original static JavaScript implementation has been converted to:
|
||||||
- Safari (latest)
|
|
||||||
|
|
||||||
## Development Tips
|
1. **Component-based Architecture**: Each major feature is now a reusable component
|
||||||
|
2. **Type Safety**: Full TypeScript support with proper interfaces
|
||||||
|
3. **Reactive Programming**: Uses RxJS observables for API calls
|
||||||
|
4. **State Management**: Centralized state management through services
|
||||||
|
5. **Modular Design**: Easy to extend and maintain
|
||||||
|
|
||||||
1. **Hot Reload**: Changes to TypeScript files automatically trigger recompilation
|
### Benefits
|
||||||
2. **Type Safety**: Use TypeScript interfaces in `models/` for all API responses
|
- **Maintainability**: Clear separation of concerns
|
||||||
3. **State Management**: Currently using component-level state; consider NgRx for complex state
|
- **Reusability**: Components can be reused and extended
|
||||||
4. **Testing**: Run `npm test` for unit tests (Jasmine/Karma)
|
- **Testing**: Angular's testing framework support
|
||||||
|
- **Performance**: Optimized change detection and lazy loading
|
||||||
|
- **Developer Experience**: Hot reload, TypeScript, and Angular DevTools
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
For production deployment, build the application and serve the `dist/frontend/browser` directory with your web server (nginx, Apache, etc.).
|
### Development
|
||||||
|
The Angular frontend can be served during development using `ng serve` and will proxy API calls to the backend.
|
||||||
|
|
||||||
Example nginx configuration:
|
### Production
|
||||||
|
Build the application and serve the static files from any web server. Ensure the backend API is accessible from the same domain or configure CORS appropriately.
|
||||||
|
|
||||||
```nginx
|
### Integration with Existing Backend
|
||||||
server {
|
The Angular frontend is designed to be a drop-in replacement for the static frontend. Simply:
|
||||||
listen 80;
|
|
||||||
server_name your-domain.com;
|
|
||||||
root /path/to/dist/frontend/browser;
|
|
||||||
|
|
||||||
location / {
|
1. Build the Angular app: `npm run build`
|
||||||
try_files $uri $uri/ /index.html;
|
2. Copy contents of `dist/frontend/` to your static files directory
|
||||||
}
|
3. Update your backend to serve the new `index.html`
|
||||||
|
4. Ensure API endpoints match the expected interface
|
||||||
|
|
||||||
location /api {
|
## Browser Support
|
||||||
proxy_pass http://backend:8000;
|
- Chrome (latest)
|
||||||
}
|
- Firefox (latest)
|
||||||
}
|
- Safari (latest)
|
||||||
```
|
- Edge (latest)
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
- Drag and drop file upload
|
||||||
- [ ] Add NgRx for state management
|
- Bulk operations
|
||||||
- [ ] Implement WebSocket for real-time updates
|
- Advanced data visualization
|
||||||
- [ ] Add Angular Material components
|
- Real-time updates via WebSocket
|
||||||
- [ ] Unit and E2E tests
|
- Export functionality
|
||||||
- [ ] PWA support
|
- User authentication integration
|
||||||
- [ ] Drag-and-drop file upload
|
|
||||||
- [ ] Bulk operations
|
|
||||||
- [ ] Export to CSV/JSON
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Same as parent project
|
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
"cli": {
|
||||||
|
"packageManager": "npm",
|
||||||
|
"analytics": false
|
||||||
|
},
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {},
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/frontend",
|
"outputPath": "dist/frontend",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
@@ -20,6 +28,7 @@
|
|||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -27,14 +36,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
]
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"optimization" : {
|
|
||||||
"fonts": false
|
|
||||||
},
|
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
@@ -58,7 +66,10 @@
|
|||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"proxyConfig": "proxy.conf.json"
|
||||||
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "frontend:build:production"
|
"buildTarget": "frontend:build:production"
|
||||||
@@ -70,16 +81,17 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular/build:extract-i18n"
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
"zone.js/testing"
|
"zone.js/testing"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -87,14 +99,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
]
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"analytics": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15273
frontend/package-lock.json
generated
Normal file
15273
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,81 +3,37 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"build:prod": "ng build --configuration production",
|
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.html",
|
|
||||||
"options": {
|
|
||||||
"parser": "angular"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "19.2.x",
|
"@angular/cdk": "^19.2.19",
|
||||||
"@angular/compiler": "19.2.x",
|
"@angular/common": "^19.2.0",
|
||||||
"@angular/core": "19.2.x",
|
"@angular/compiler": "^19.2.0",
|
||||||
"@angular/forms": "19.2.x",
|
"@angular/core": "^19.2.0",
|
||||||
"@angular/platform-browser": "19.2.x",
|
"@angular/forms": "^19.2.0",
|
||||||
"@angular/router": "19.2.x",
|
"@angular/material": "^19.2.19",
|
||||||
|
"@angular/platform-browser": "^19.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
|
"@angular/router": "^19.2.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "19.2.x",
|
"@angular-devkit/build-angular": "^19.2.17",
|
||||||
"@angular/cli": "19.2.x",
|
"@angular/cli": "^19.2.17",
|
||||||
"@angular/compiler-cli": "19.2.x",
|
"@angular/compiler-cli": "^19.2.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.9.0",
|
"jasmine-core": "~5.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "5.x.x",
|
"typescript": "~5.7.2"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
"/api": {
|
"/api": {
|
||||||
"target": "http://localhost:8000",
|
"target": "http://localhost:8000",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true
|
"changeOrigin": true,
|
||||||
|
"logLevel": "debug",
|
||||||
|
"pathRewrite": {
|
||||||
|
"^/api": "/api"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
125
frontend/src/app/app.component.html
Normal file
125
frontend/src/app/app.component.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<div class="app-container">
|
||||||
|
<!-- Material Toolbar Header -->
|
||||||
|
<mat-toolbar color="primary" class="app-toolbar">
|
||||||
|
<mat-icon class="app-icon">storage</mat-icon>
|
||||||
|
<span class="app-title">{{ title }}</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<div class="header-info" *ngIf="apiInfo">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip>
|
||||||
|
<mat-icon matChipAvatar>settings</mat-icon>
|
||||||
|
{{ apiInfo.deployment_mode }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip>
|
||||||
|
<mat-icon matChipAvatar>folder</mat-icon>
|
||||||
|
{{ apiInfo.storage_backend }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<mat-tab-group [(selectedIndex)]="selectedTabIndex" class="main-tabs">
|
||||||
|
<!-- Artifacts Tab -->
|
||||||
|
<mat-tab label="Artifacts">
|
||||||
|
<ng-template matTabContent>
|
||||||
|
<div class="tab-content-wrapper">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>storage</mat-icon>
|
||||||
|
Artifacts ({{ artifacts.length }})
|
||||||
|
</h2>
|
||||||
|
<button mat-raised-button color="primary" (click)="loadArtifacts()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table mat-table [dataSource]="artifacts" class="artifacts-table mat-elevation-z4">
|
||||||
|
<!-- ID Column -->
|
||||||
|
<ng-container matColumnDef="id">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>ID</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">{{ artifact.id }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Filename Column -->
|
||||||
|
<ng-container matColumnDef="filename">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Filename</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact" class="filename-cell">
|
||||||
|
<mat-icon class="file-icon">description</mat-icon>
|
||||||
|
{{ artifact.filename }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Type Column -->
|
||||||
|
<ng-container matColumnDef="file_type">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<mat-chip class="type-chip">{{ artifact.file_type }}</mat-chip>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Size Column -->
|
||||||
|
<ng-container matColumnDef="file_size">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Size</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">{{ formatBytes(artifact.file_size) }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Test Name Column -->
|
||||||
|
<ng-container matColumnDef="test_name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Test Name</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">{{ artifact.test_name || '-' }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Test Result Column -->
|
||||||
|
<ng-container matColumnDef="test_result">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Result</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<mat-chip *ngIf="artifact.test_result" [class]="'result-' + artifact.test_result">
|
||||||
|
{{ artifact.test_result }}
|
||||||
|
</mat-chip>
|
||||||
|
<span *ngIf="!artifact.test_result" class="text-muted">-</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- Upload Tab -->
|
||||||
|
<mat-tab label="Upload">
|
||||||
|
<ng-template matTabContent>
|
||||||
|
<div class="tab-content-wrapper">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
|
Upload Artifacts
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<app-upload-form (uploadSuccess)="onUploadSuccess()"></app-upload-form>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- Query Tab -->
|
||||||
|
<mat-tab label="Query">
|
||||||
|
<ng-template matTabContent>
|
||||||
|
<div class="tab-content-wrapper">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2>
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
Query Artifacts
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<app-query-form
|
||||||
|
(queryResults)="onQueryResults($event)"
|
||||||
|
(filtersChange)="onFiltersChange($event)">
|
||||||
|
</app-query-form>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
</div>
|
||||||
244
frontend/src/app/app.component.scss
Normal file
244
frontend/src/app/app.component.scss
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
mat-chip-set {
|
||||||
|
mat-chip {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: white !important;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Group Styling
|
||||||
|
.main-tabs {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-mdc-tab-body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-header {
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-labels {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab {
|
||||||
|
min-width: 120px;
|
||||||
|
height: 56px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Content Wrapper
|
||||||
|
.tab-content-wrapper {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Header
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
color: #3f51b5;
|
||||||
|
font-size: 28px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifacts Table Styling
|
||||||
|
.artifacts-table {
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mat-mdc-cell {
|
||||||
|
padding: 16px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.mat-mdc-row {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
color: #3f51b5;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-chip {
|
||||||
|
background-color: #e3f2fd !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result Chips
|
||||||
|
.result-pass {
|
||||||
|
background-color: #e8f5e9 !important;
|
||||||
|
color: #2e7d32 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-fail {
|
||||||
|
background-color: #ffebee !important;
|
||||||
|
color: #c62828 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-skip {
|
||||||
|
background-color: #fff3e0 !important;
|
||||||
|
color: #ef6c00 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background-color: #fce4ec !important;
|
||||||
|
color: #c2185b !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tab-content-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-table {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
th.mat-mdc-header-cell,
|
||||||
|
td.mat-mdc-cell {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.artifacts-table {
|
||||||
|
th.mat-mdc-header-cell:nth-child(n+5),
|
||||||
|
td.mat-mdc-cell:nth-child(n+5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,27 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { App } from './app';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [App],
|
imports: [AppComponent],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create the app', () => {
|
it('should create the app', () => {
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should have the 'frontend' title`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('frontend');
|
||||||
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
it('should render title', () => {
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
|
||||||
93
frontend/src/app/app.component.ts
Normal file
93
frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { UploadFormComponent } from './components/upload-form/upload-form.component';
|
||||||
|
import { QueryFormComponent } from './components/query-form/query-form.component';
|
||||||
|
import { ApiService } from './services/api.service';
|
||||||
|
import { ArtifactService } from './services/artifact.service';
|
||||||
|
import { ApiInfo, Artifact } from './models/artifact.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
UploadFormComponent,
|
||||||
|
QueryFormComponent
|
||||||
|
],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.scss'
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnInit {
|
||||||
|
title = 'Test Artifact Data Lake';
|
||||||
|
apiInfo: ApiInfo | null = null;
|
||||||
|
artifacts: Artifact[] = [];
|
||||||
|
displayedColumns: string[] = ['id', 'filename', 'file_type', 'file_size', 'test_name', 'test_result'];
|
||||||
|
selectedTabIndex = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService,
|
||||||
|
private artifactService: ArtifactService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadApiInfo();
|
||||||
|
this.loadArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadApiInfo(): void {
|
||||||
|
this.apiService.getApiInfo().subscribe({
|
||||||
|
next: (info) => {
|
||||||
|
this.apiInfo = info;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading API info:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadArtifacts(): void {
|
||||||
|
this.artifactService.getArtifacts().subscribe({
|
||||||
|
next: (artifacts) => {
|
||||||
|
console.log('Loaded artifacts:', artifacts.length);
|
||||||
|
this.artifacts = artifacts;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading artifacts:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadSuccess(): void {
|
||||||
|
this.loadArtifacts();
|
||||||
|
this.selectedTabIndex = 0; // Switch back to artifacts tab
|
||||||
|
}
|
||||||
|
|
||||||
|
onQueryResults(artifacts: Artifact[]): void {
|
||||||
|
this.artifacts = artifacts;
|
||||||
|
this.selectedTabIndex = 0; // Switch to artifacts tab to show results
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersChange(filters: any): void {
|
||||||
|
// Filters will be handled by the query component
|
||||||
|
console.log('Filters changed:', filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * 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 />
|
|
||||||
@@ -1,11 +1,3 @@
|
|||||||
import { Routes } from '@angular/router';
|
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';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [];
|
||||||
{ path: '', redirectTo: '/artifacts', pathMatch: 'full' },
|
|
||||||
{ path: 'artifacts', component: ArtifactsListComponent },
|
|
||||||
{ path: 'upload', component: UploadFormComponent },
|
|
||||||
{ path: 'query', component: QueryFormComponent }
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
|
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
|
||||||
import { ArtifactService } from './services/artifact';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
|
||||||
template: `
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1><span class="logo">[W13]</span></h1>
|
|
||||||
<div class="header-info">
|
|
||||||
<span class="badge">{{ deploymentMode }}</span>
|
|
||||||
<span class="badge">{{ storageBackend }}</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav class="tabs">
|
|
||||||
<a routerLink="/artifacts" routerLinkActive="active" class="tab-button">
|
|
||||||
<span class="material-icons md-16">storage</span> Artifacts
|
|
||||||
</a>
|
|
||||||
<a routerLink="/upload" routerLinkActive="active" class="tab-button">
|
|
||||||
<span class="material-icons md-16">upload</span> Upload
|
|
||||||
</a>
|
|
||||||
<a routerLink="/query" routerLinkActive="active" class="tab-button">
|
|
||||||
<span class="material-icons md-16">search</span> Query
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styleUrls: ['./app.css']
|
|
||||||
})
|
|
||||||
export class AppComponent implements OnInit {
|
|
||||||
deploymentMode: string = '';
|
|
||||||
storageBackend: string = '';
|
|
||||||
|
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<div class="artifacts-section">
|
||||||
|
<!-- Debug Loading State -->
|
||||||
|
<div style="background: red; color: white; padding: 10px; margin: 10px;">
|
||||||
|
LOADING STATE: {{ loading ? 'TRUE (Loading...)' : 'FALSE (Not Loading)' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<mat-card class="toolbar-card">
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-buttons">
|
||||||
|
<button mat-raised-button color="primary" (click)="loadArtifacts()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="accent" (click)="generateSeedData()">
|
||||||
|
<mat-icon>scatter_plot</mat-icon>
|
||||||
|
Generate Seed Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-chip-set class="count-chip">
|
||||||
|
<mat-chip>
|
||||||
|
<mat-icon matChipAvatar>storage</mat-icon>
|
||||||
|
{{ filteredArtifacts.length }} artifacts
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Loading Spinner -->
|
||||||
|
<div *ngIf="loading" class="loading-container">
|
||||||
|
<mat-spinner diameter="50"></mat-spinner>
|
||||||
|
<p>Loading artifacts...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple List Test (No Material Components) -->
|
||||||
|
<div *ngIf="!loading" style="background: lightgreen; padding: 20px; margin: 20px;">
|
||||||
|
<h3>Simple List Test</h3>
|
||||||
|
<p><strong>Filtered Artifacts Count:</strong> {{ filteredArtifacts.length }}</p>
|
||||||
|
<div *ngFor="let artifact of filteredArtifacts.slice(0, 5)" style="border: 1px solid black; padding: 10px; margin: 5px;">
|
||||||
|
<strong>ID:</strong> {{ artifact.id }} |
|
||||||
|
<strong>Filename:</strong> {{ artifact.filename }} |
|
||||||
|
<strong>Type:</strong> {{ artifact.file_type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Material Table -->
|
||||||
|
<mat-card *ngIf="!loading" class="table-card">
|
||||||
|
<div class="table-container">
|
||||||
|
<table mat-table [dataSource]="filteredArtifacts" class="artifacts-table">
|
||||||
|
|
||||||
|
<!-- ID Column -->
|
||||||
|
<ng-container matColumnDef="id">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>ID</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<strong>{{ artifact.id }}</strong>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Event ID Column -->
|
||||||
|
<ng-container matColumnDef="eventId">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Event ID</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<mat-chip *ngIf="artifact.event_id" color="primary">
|
||||||
|
{{ artifact.event_id }}
|
||||||
|
</mat-chip>
|
||||||
|
<span *ngIf="!artifact.event_id" class="text-muted">{{ artifact.id }}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Filename Column -->
|
||||||
|
<ng-container matColumnDef="filename">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Filename</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<button mat-button (click)="showDetail(artifact)" class="filename-link">
|
||||||
|
<mat-icon>description</mat-icon>
|
||||||
|
{{ artifact.filename }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Type Column -->
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Type</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<mat-chip class="type-chip">{{ artifact.file_type }}</mat-chip>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Size Column -->
|
||||||
|
<ng-container matColumnDef="size">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Size</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">{{ formatBytes(artifact.file_size) }}</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Binaries Column -->
|
||||||
|
<ng-container matColumnDef="binaries">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Binaries</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact" class="binaries-cell">
|
||||||
|
<div *ngIf="artifact.binaries && artifact.binaries.length > 0; else noBinaries">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip *ngFor="let binary of getVisibleBinaries(artifact.binaries)" class="binary-chip">
|
||||||
|
<mat-icon matChipAvatar>code</mat-icon>
|
||||||
|
{{ binary }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip
|
||||||
|
*ngIf="getHiddenBinariesCount(artifact.binaries) > 0"
|
||||||
|
(click)="toggleBinariesExpansion(artifact.id)"
|
||||||
|
class="expand-chip">
|
||||||
|
<span *ngIf="!expandedBinaries[artifact.id]">
|
||||||
|
+{{ getHiddenBinariesCount(artifact.binaries) }} more
|
||||||
|
</span>
|
||||||
|
<span *ngIf="expandedBinaries[artifact.id]">- less</span>
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
<mat-chip-set *ngIf="expandedBinaries[artifact.id]" class="expanded-binaries">
|
||||||
|
<mat-chip *ngFor="let binary of artifact.binaries.slice(4)" class="binary-chip">
|
||||||
|
<mat-icon matChipAvatar>code</mat-icon>
|
||||||
|
{{ binary }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
<ng-template #noBinaries>
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Test Name Column -->
|
||||||
|
<ng-container matColumnDef="testName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Test Name</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<span>{{ artifact.test_name || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Suite Column -->
|
||||||
|
<ng-container matColumnDef="suite">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Suite</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<span>{{ artifact.test_suite || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Result Column -->
|
||||||
|
<ng-container matColumnDef="result">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Result</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<mat-chip
|
||||||
|
*ngIf="artifact.test_result"
|
||||||
|
[class]="'result-' + artifact.test_result">
|
||||||
|
<mat-icon matChipAvatar>{{ getResultIcon(artifact.test_result) }}</mat-icon>
|
||||||
|
{{ artifact.test_result }}
|
||||||
|
</mat-chip>
|
||||||
|
<span *ngIf="!artifact.test_result" class="text-muted">-</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Tags Column -->
|
||||||
|
<ng-container matColumnDef="tags">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Tags</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact" class="tags-cell">
|
||||||
|
<div *ngIf="artifact.tags && artifact.tags.length > 0; else noTags">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip *ngFor="let tag of getVisibleTags(artifact.tags)" class="tag-chip">
|
||||||
|
{{ tag }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip
|
||||||
|
*ngIf="getHiddenTagsCount(artifact.tags) > 0"
|
||||||
|
(click)="toggleTagsExpansion(artifact.id)"
|
||||||
|
class="expand-chip">
|
||||||
|
<span *ngIf="!expandedTags[artifact.id]">
|
||||||
|
+{{ getHiddenTagsCount(artifact.tags) }} more
|
||||||
|
</span>
|
||||||
|
<span *ngIf="expandedTags[artifact.id]">- less</span>
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
<mat-chip-set *ngIf="expandedTags[artifact.id]" class="expanded-tags">
|
||||||
|
<mat-chip *ngFor="let tag of artifact.tags.slice(3)" class="tag-chip">
|
||||||
|
{{ tag }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
<app-tag-manager
|
||||||
|
[artifactId]="artifact.id"
|
||||||
|
[currentTags]="artifact.tags"
|
||||||
|
(tagsUpdated)="onTagsUpdated()">
|
||||||
|
</app-tag-manager>
|
||||||
|
</div>
|
||||||
|
<ng-template #noTags>
|
||||||
|
<app-tag-manager
|
||||||
|
[artifactId]="artifact.id"
|
||||||
|
[currentTags]="[]"
|
||||||
|
(tagsUpdated)="onTagsUpdated()">
|
||||||
|
</app-tag-manager>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Created Column -->
|
||||||
|
<ng-container matColumnDef="created">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Created</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<small>{{ formatDate(artifact.created_at) }}</small>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Actions Column -->
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||||
|
<td mat-cell *matCellDef="let artifact">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="downloadArtifact(artifact)"
|
||||||
|
matTooltip="Download"
|
||||||
|
color="primary">
|
||||||
|
<mat-icon>download</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="deleteArtifact(artifact)"
|
||||||
|
matTooltip="Delete"
|
||||||
|
color="warn">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Table Header and Rows -->
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
|
||||||
|
<!-- No Data Row -->
|
||||||
|
<tr class="mat-row" *matNoDataRow>
|
||||||
|
<td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
|
||||||
|
<div class="no-data-content">
|
||||||
|
<mat-icon>inbox</mat-icon>
|
||||||
|
<p>No artifacts found. Upload some files to get started!</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<mat-card class="pagination-card" *ngIf="!loading">
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="pagination">
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="previousPage()"
|
||||||
|
[disabled]="currentPage === 1"
|
||||||
|
matTooltip="Previous page">
|
||||||
|
<mat-icon>chevron_left</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-chip>Page {{ currentPage }}</mat-chip>
|
||||||
|
<button mat-icon-button
|
||||||
|
(click)="nextPage()"
|
||||||
|
[disabled]="filteredArtifacts.length < pageSize"
|
||||||
|
matTooltip="Next page">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Artifact Detail Modal (placeholder for now) -->
|
||||||
|
<div *ngIf="showDetailModal && selectedArtifact" class="detail-backdrop" (click)="closeDetailModal()">
|
||||||
|
<mat-card class="detail-modal" (click)="$event.stopPropagation()">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>Artifact Details</mat-card-title>
|
||||||
|
<button mat-icon-button (click)="closeDetailModal()" class="close-button">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<!-- Detail content will be added later -->
|
||||||
|
<p>Artifact ID: {{ selectedArtifact.id }}</p>
|
||||||
|
<p>Filename: {{ selectedArtifact.filename }}</p>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
.artifacts-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-chip {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mat-mdc-cell,
|
||||||
|
.mat-mdc-header-cell {
|
||||||
|
padding: 12px 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-header-cell {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-row:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column specific styles
|
||||||
|
.binaries-cell,
|
||||||
|
.tags-cell {
|
||||||
|
max-width: 250px;
|
||||||
|
|
||||||
|
mat-chip-set {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename-link {
|
||||||
|
text-align: left;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chip styles
|
||||||
|
.type-chip {
|
||||||
|
background-color: #e3f2fd !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-chip {
|
||||||
|
background-color: #f3e5f5 !important;
|
||||||
|
color: #7b1fa2 !important;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
background-color: #e8f5e8 !important;
|
||||||
|
color: #2e7d32 !important;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-chip {
|
||||||
|
background-color: #fff3e0 !important;
|
||||||
|
color: #f57c00 !important;
|
||||||
|
font-size: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ffe0b2 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-binaries,
|
||||||
|
.expanded-tags {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result chips
|
||||||
|
.result-pass {
|
||||||
|
background-color: #e8f5e8 !important;
|
||||||
|
color: #2e7d32 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-fail {
|
||||||
|
background-color: #ffebee !important;
|
||||||
|
color: #d32f2f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-skip {
|
||||||
|
background-color: #fff8e1 !important;
|
||||||
|
color: #f57c00 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background-color: #fce4ec !important;
|
||||||
|
color: #c2185b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No data state
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
.pagination-card {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail modal
|
||||||
|
.detail-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-modal {
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-chip {
|
||||||
|
margin-left: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-table {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.mat-mdc-cell,
|
||||||
|
.mat-mdc-header-cell {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.binaries-cell,
|
||||||
|
.tags-cell {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override Material styles
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-table {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-chip {
|
||||||
|
--mdc-chip-container-height: 24px;
|
||||||
|
--mdc-chip-with-avatar-container-height: 28px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-chip-set {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { Artifact } from '../../models/artifact.interface';
|
||||||
|
import { ArtifactService } from '../../services/artifact.service';
|
||||||
|
import { NotificationService } from '../../services/notification.service';
|
||||||
|
import { TagManagerComponent } from '../tag-manager/tag-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-artifacts-table',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
TagManagerComponent
|
||||||
|
],
|
||||||
|
templateUrl: './artifacts-table.component.html',
|
||||||
|
styleUrl: './artifacts-table.component.scss'
|
||||||
|
})
|
||||||
|
export class ArtifactsTableComponent implements OnInit, OnChanges {
|
||||||
|
@Input() artifacts: Artifact[] = [];
|
||||||
|
@Input() filters: any = {};
|
||||||
|
|
||||||
|
displayedColumns: string[] = [
|
||||||
|
'id',
|
||||||
|
'eventId',
|
||||||
|
'filename',
|
||||||
|
'type',
|
||||||
|
'size',
|
||||||
|
'binaries',
|
||||||
|
'testName',
|
||||||
|
'suite',
|
||||||
|
'result',
|
||||||
|
'tags',
|
||||||
|
'created',
|
||||||
|
'actions'
|
||||||
|
];
|
||||||
|
|
||||||
|
expandedBinaries: { [key: number]: boolean } = {};
|
||||||
|
expandedTags: { [key: number]: boolean } = {};
|
||||||
|
|
||||||
|
currentPage = 1;
|
||||||
|
pageSize = 25;
|
||||||
|
loading = false; // Start with false to show content immediately
|
||||||
|
|
||||||
|
selectedArtifact: Artifact | null = null;
|
||||||
|
showDetailModal = false;
|
||||||
|
|
||||||
|
filteredArtifacts: Artifact[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private artifactService: ArtifactService,
|
||||||
|
private notificationService: NotificationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
console.log('ArtifactsTableComponent ngOnInit - artifacts count:', this.artifacts.length);
|
||||||
|
console.log('Initial loading state:', this.loading);
|
||||||
|
// Always load artifacts on init
|
||||||
|
this.loadArtifacts();
|
||||||
|
// Force show after a delay to debug
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Timeout - forcing loading to false');
|
||||||
|
this.loading = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
console.log('ArtifactsTableComponent ngOnChanges - artifacts:', changes['artifacts']?.currentValue?.length || 0);
|
||||||
|
// Re-apply filters when artifacts or filters input changes
|
||||||
|
if (changes['artifacts'] || changes['filters']) {
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadArtifacts(): void {
|
||||||
|
console.log('Loading artifacts...');
|
||||||
|
this.loading = true;
|
||||||
|
this.artifactService.getArtifacts(this.pageSize, (this.currentPage - 1) * this.pageSize)
|
||||||
|
.subscribe({
|
||||||
|
next: (artifacts) => {
|
||||||
|
console.log('Loaded artifacts:', artifacts.length);
|
||||||
|
this.artifacts = artifacts;
|
||||||
|
this.applyFilters();
|
||||||
|
this.loading = false;
|
||||||
|
console.log('Loading complete. loading =', this.loading);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading artifacts:', error);
|
||||||
|
this.loading = false;
|
||||||
|
console.log('Error occurred. loading =', this.loading);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilters(): void {
|
||||||
|
console.log('Applying filters to', this.artifacts.length, 'artifacts');
|
||||||
|
this.filteredArtifacts = this.artifacts.filter(artifact => {
|
||||||
|
if (this.filters.filename && !artifact.filename.toLowerCase().includes(this.filters.filename.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.filters.fileType && artifact.file_type !== this.filters.fileType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.filters.testName && !artifact.test_name?.toLowerCase().includes(this.filters.testName.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.filters.testSuite && !artifact.test_suite?.toLowerCase().includes(this.filters.testSuite.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.filters.testResult && artifact.test_result !== this.filters.testResult) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.filters.tags && this.filters.tags.length > 0) {
|
||||||
|
const hasMatchingTag = this.filters.tags.some((tag: string) =>
|
||||||
|
artifact.tags.some(artifactTag => artifactTag.toLowerCase().includes(tag.toLowerCase()))
|
||||||
|
);
|
||||||
|
if (!hasMatchingTag) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
console.log('Filtered artifacts count:', this.filteredArtifacts.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBinariesExpansion(artifactId: number): void {
|
||||||
|
this.expandedBinaries[artifactId] = !this.expandedBinaries[artifactId];
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTagsExpansion(artifactId: number): void {
|
||||||
|
this.expandedTags[artifactId] = !this.expandedTags[artifactId];
|
||||||
|
}
|
||||||
|
|
||||||
|
showDetail(artifact: Artifact): void {
|
||||||
|
this.selectedArtifact = artifact;
|
||||||
|
this.showDetailModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDetailModal(): void {
|
||||||
|
this.showDetailModal = false;
|
||||||
|
this.selectedArtifact = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadArtifact(artifact: Artifact): void {
|
||||||
|
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: (error) => {
|
||||||
|
console.error('Error downloading artifact:', error);
|
||||||
|
this.notificationService.showError('Error downloading artifact: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteArtifact(artifact: Artifact): Promise<void> {
|
||||||
|
const confirmed = await this.notificationService.showConfirmation(
|
||||||
|
`Are you sure you want to delete "${artifact.filename}"? This cannot be undone.`,
|
||||||
|
'Delete'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.artifactService.deleteArtifact(artifact.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.notificationService.showSuccess('Artifact deleted successfully');
|
||||||
|
this.loadArtifacts();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting artifact:', error);
|
||||||
|
this.notificationService.showError('Error deleting artifact: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSeedData(): void {
|
||||||
|
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) {
|
||||||
|
this.notificationService.showWarning('Please enter a number between 1 and 100');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.artifactService.generateSeedData(num).subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
this.notificationService.showSuccess(result.message || 'Seed data generated successfully');
|
||||||
|
this.loadArtifacts();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error generating seed data:', error);
|
||||||
|
this.notificationService.showError('Error generating seed data: ' + error.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 {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleBinaries(binaries: string[] | undefined): string[] {
|
||||||
|
if (!binaries) return [];
|
||||||
|
return binaries.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiddenBinariesCount(binaries: string[] | undefined): number {
|
||||||
|
if (!binaries) return 0;
|
||||||
|
return Math.max(0, binaries.length - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisibleTags(tags: string[]): string[] {
|
||||||
|
return tags.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHiddenTagsCount(tags: string[]): number {
|
||||||
|
return Math.max(0, tags.length - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPage(): void {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.currentPage--;
|
||||||
|
this.loadArtifacts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage(): void {
|
||||||
|
this.currentPage++;
|
||||||
|
this.loadArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagsUpdated(): void {
|
||||||
|
this.loadArtifacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultIcon(result: string): string {
|
||||||
|
switch (result) {
|
||||||
|
case 'pass': return 'check_circle';
|
||||||
|
case 'fail': return 'cancel';
|
||||||
|
case 'skip': return 'skip_next';
|
||||||
|
case 'error': return 'error';
|
||||||
|
default: return 'help';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
197
frontend/src/app/components/query-form/query-form.component.html
Normal file
197
frontend/src/app/components/query-form/query-form.component.html
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<mat-card class="query-card">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
Query Artifacts
|
||||||
|
</mat-card-title>
|
||||||
|
<mat-card-subtitle>
|
||||||
|
Search and filter your artifact collection
|
||||||
|
</mat-card-subtitle>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<form (ngSubmit)="queryArtifacts()" #queryFormRef="ngForm" class="query-form">
|
||||||
|
<!-- Basic Search Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Basic Search</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Filename</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="filename"
|
||||||
|
[(ngModel)]="queryForm.filename"
|
||||||
|
(input)="onFilterChange()"
|
||||||
|
placeholder="Search filename...">
|
||||||
|
<mat-icon matSuffix>description</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>File Type</mat-label>
|
||||||
|
<mat-select
|
||||||
|
name="file_type"
|
||||||
|
[(ngModel)]="queryForm.file_type"
|
||||||
|
(selectionChange)="onFilterChange()">
|
||||||
|
<mat-option value="">All Types</mat-option>
|
||||||
|
<mat-option *ngFor="let type of fileTypes" [value]="type">
|
||||||
|
{{ type.toUpperCase() }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-icon matSuffix>category</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Information Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Test Information</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Name</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="test_name"
|
||||||
|
[(ngModel)]="queryForm.test_name"
|
||||||
|
(input)="onFilterChange()"
|
||||||
|
placeholder="Search test name...">
|
||||||
|
<mat-icon matSuffix>quiz</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Suite</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="test_suite"
|
||||||
|
[(ngModel)]="queryForm.test_suite"
|
||||||
|
(input)="onFilterChange()"
|
||||||
|
placeholder="e.g., integration">
|
||||||
|
<mat-icon matSuffix>folder</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Result</mat-label>
|
||||||
|
<mat-select
|
||||||
|
name="test_result"
|
||||||
|
[(ngModel)]="queryForm.test_result"
|
||||||
|
(selectionChange)="onFilterChange()">
|
||||||
|
<mat-option value="">All Results</mat-option>
|
||||||
|
<mat-option *ngFor="let result of testResults" [value]="result">
|
||||||
|
<mat-icon>{{ getResultIcon(result) }}</mat-icon>
|
||||||
|
{{ result | titlecase }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-icon matSuffix>assignment_turned_in</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Tags</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="tags"
|
||||||
|
[(ngModel)]="tagsInput"
|
||||||
|
(input)="onFilterChange()"
|
||||||
|
placeholder="e.g., regression, smoke">
|
||||||
|
<mat-icon matSuffix>label</mat-icon>
|
||||||
|
<mat-hint>Comma-separated tags</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Date Range</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Start Date</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="startPicker"
|
||||||
|
name="start_date"
|
||||||
|
[(ngModel)]="queryForm.start_date">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #startPicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>End Date</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="endPicker"
|
||||||
|
name="end_date"
|
||||||
|
[(ngModel)]="queryForm.end_date">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #endPicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Progress -->
|
||||||
|
<div class="search-progress" *ngIf="searching">
|
||||||
|
<mat-spinner diameter="24"></mat-spinner>
|
||||||
|
<span>Searching artifacts...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="searching"
|
||||||
|
class="search-button">
|
||||||
|
<mat-icon>{{ searching ? 'hourglass_empty' : 'search' }}</mat-icon>
|
||||||
|
{{ searching ? 'Searching...' : 'Search Artifacts' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
type="button"
|
||||||
|
(click)="clearQuery()"
|
||||||
|
[disabled]="searching">
|
||||||
|
<mat-icon>clear</mat-icon>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Active Filters Display -->
|
||||||
|
<mat-card class="filters-card" *ngIf="queryForm.filename || queryForm.file_type || queryForm.test_name || queryForm.test_suite || queryForm.test_result || tagsInput">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>
|
||||||
|
<mat-icon>filter_list</mat-icon>
|
||||||
|
Active Filters
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<mat-chip-set class="filter-chips">
|
||||||
|
<mat-chip *ngIf="queryForm.filename" color="primary">
|
||||||
|
<mat-icon matChipAvatar>description</mat-icon>
|
||||||
|
Filename: {{ queryForm.filename }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip *ngIf="queryForm.file_type" color="accent">
|
||||||
|
<mat-icon matChipAvatar>category</mat-icon>
|
||||||
|
Type: {{ queryForm.file_type }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip *ngIf="queryForm.test_name" color="primary">
|
||||||
|
<mat-icon matChipAvatar>quiz</mat-icon>
|
||||||
|
Test: {{ queryForm.test_name }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip *ngIf="queryForm.test_suite" color="accent">
|
||||||
|
<mat-icon matChipAvatar>folder</mat-icon>
|
||||||
|
Suite: {{ queryForm.test_suite }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip *ngIf="queryForm.test_result" color="primary">
|
||||||
|
<mat-icon matChipAvatar>assignment_turned_in</mat-icon>
|
||||||
|
Result: {{ queryForm.test_result }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip *ngIf="tagsInput" color="accent">
|
||||||
|
<mat-icon matChipAvatar>label</mat-icon>
|
||||||
|
Tags: {{ tagsInput }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
207
frontend/src/app/components/query-form/query-form.component.scss
Normal file
207
frontend/src/app/components/query-form/query-form.component.scss
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
.query-card {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-card {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #424242;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
mat-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
padding: 12px 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
mat-chip {
|
||||||
|
--mdc-chip-container-height: 36px;
|
||||||
|
|
||||||
|
mat-icon[matChipAvatar] {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: currentColor !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material Design overrides
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-form-field {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mat-mdc-form-field-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-select-panel {
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-chip {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-raised-button {
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-datepicker-toggle {
|
||||||
|
.mat-icon-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
.mat-mdc-button-touch-target {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card title styling
|
||||||
|
:host ::ng-deep .mat-mdc-card-header {
|
||||||
|
.mat-mdc-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-card-subtitle {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.query-card,
|
||||||
|
.filters-card {
|
||||||
|
margin: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips {
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
mat-chip {
|
||||||
|
--mdc-chip-container-height: 32px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.query-card,
|
||||||
|
.filters-card {
|
||||||
|
margin: 0 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
frontend/src/app/components/query-form/query-form.component.ts
Normal file
137
frontend/src/app/components/query-form/query-form.component.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { Component, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
|
import { ArtifactQuery, Artifact } from '../../models/artifact.interface';
|
||||||
|
import { ArtifactService } from '../../services/artifact.service';
|
||||||
|
import { NotificationService } from '../../services/notification.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-query-form',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatNativeDateModule
|
||||||
|
],
|
||||||
|
templateUrl: './query-form.component.html',
|
||||||
|
styleUrl: './query-form.component.scss'
|
||||||
|
})
|
||||||
|
export class QueryFormComponent {
|
||||||
|
@Output() queryResults = new EventEmitter<Artifact[]>();
|
||||||
|
@Output() filtersChange = new EventEmitter<any>();
|
||||||
|
|
||||||
|
queryForm: ArtifactQuery = {
|
||||||
|
filename: '',
|
||||||
|
file_type: '',
|
||||||
|
test_name: '',
|
||||||
|
test_suite: '',
|
||||||
|
test_result: '',
|
||||||
|
tags: [],
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
limit: 100,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
searching = false;
|
||||||
|
fileTypes = ['csv', 'json', 'binary', 'pcap'];
|
||||||
|
testResults = ['pass', 'fail', 'skip', 'error'];
|
||||||
|
|
||||||
|
tagsInput = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private artifactService: ArtifactService,
|
||||||
|
private notificationService: NotificationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
queryArtifacts(): void {
|
||||||
|
this.searching = true;
|
||||||
|
|
||||||
|
const query: ArtifactQuery = { ...this.queryForm };
|
||||||
|
|
||||||
|
if (this.tagsInput) {
|
||||||
|
query.tags = this.tagsInput.split(',').map(t => t.trim()).filter(t => t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.start_date) {
|
||||||
|
query.start_date = new Date(query.start_date).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.end_date) {
|
||||||
|
query.end_date = new Date(query.end_date).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.artifactService.queryArtifacts(query).subscribe({
|
||||||
|
next: (artifacts) => {
|
||||||
|
this.queryResults.emit(artifacts);
|
||||||
|
this.searching = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Query failed:', error);
|
||||||
|
this.notificationService.showError('Query failed: ' + error.message);
|
||||||
|
this.searching = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearQuery(): void {
|
||||||
|
this.queryForm = {
|
||||||
|
filename: '',
|
||||||
|
file_type: '',
|
||||||
|
test_name: '',
|
||||||
|
test_suite: '',
|
||||||
|
test_result: '',
|
||||||
|
tags: [],
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
limit: 100,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
this.tagsInput = '';
|
||||||
|
this.emitFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitFilters(): void {
|
||||||
|
const filters = {
|
||||||
|
filename: this.queryForm.filename,
|
||||||
|
fileType: this.queryForm.file_type,
|
||||||
|
testName: this.queryForm.test_name,
|
||||||
|
testSuite: this.queryForm.test_suite,
|
||||||
|
testResult: this.queryForm.test_result,
|
||||||
|
tags: this.tagsInput ? this.tagsInput.split(',').map(t => t.trim()).filter(t => t) : []
|
||||||
|
};
|
||||||
|
this.filtersChange.emit(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange(): void {
|
||||||
|
this.emitFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultIcon(result: string): string {
|
||||||
|
switch (result) {
|
||||||
|
case 'pass': return 'check_circle';
|
||||||
|
case 'fail': return 'cancel';
|
||||||
|
case 'skip': return 'skip_next';
|
||||||
|
case 'error': return 'error';
|
||||||
|
default: return 'help';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<mat-tab-group
|
||||||
|
[(selectedIndex)]="selectedIndex"
|
||||||
|
(selectedTabChange)="onTabChange($event)"
|
||||||
|
animationDuration="300ms"
|
||||||
|
color="primary">
|
||||||
|
|
||||||
|
<!-- Artifacts Tab -->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<mat-icon class="tab-icon">view_list</mat-icon>
|
||||||
|
Artifacts
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- Upload Tab -->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<mat-icon class="tab-icon">cloud_upload</mat-icon>
|
||||||
|
Upload
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<!-- Query Tab -->
|
||||||
|
<mat-tab>
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<mat-icon class="tab-icon">search</mat-icon>
|
||||||
|
Query
|
||||||
|
</ng-template>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.tab-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-tab-group {
|
||||||
|
.mat-mdc-tab-header {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab {
|
||||||
|
min-width: 120px;
|
||||||
|
|
||||||
|
.mat-mdc-tab-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-body-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-tab {
|
||||||
|
min-width: 80px;
|
||||||
|
|
||||||
|
.mat-mdc-tab-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
|
export type TabType = 'artifacts' | 'upload' | 'query';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tab-navigation',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatIconModule
|
||||||
|
],
|
||||||
|
templateUrl: './tab-navigation.component.html',
|
||||||
|
styleUrl: './tab-navigation.component.scss'
|
||||||
|
})
|
||||||
|
export class TabNavigationComponent {
|
||||||
|
@Output() tabChange = new EventEmitter<TabType>();
|
||||||
|
|
||||||
|
selectedIndex = 0;
|
||||||
|
|
||||||
|
tabs = [
|
||||||
|
{ id: 'artifacts' as TabType, label: 'Artifacts', icon: 'view_list' },
|
||||||
|
{ id: 'upload' as TabType, label: 'Upload', icon: 'cloud_upload' },
|
||||||
|
{ id: 'query' as TabType, label: 'Query', icon: 'search' }
|
||||||
|
];
|
||||||
|
|
||||||
|
onTabChange(event: any): void {
|
||||||
|
const selectedTab = this.tabs[event.index];
|
||||||
|
this.tabChange.emit(selectedTab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<div class="tag-manager">
|
||||||
|
<!-- Current Tags Display -->
|
||||||
|
<div class="current-tags-section" *ngIf="currentTags.length > 0">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let tag of currentTags"
|
||||||
|
class="current-tag"
|
||||||
|
[removable]="true"
|
||||||
|
(removed)="removeTag(tag)">
|
||||||
|
<mat-icon matChipAvatar>label</mat-icon>
|
||||||
|
{{ tag }}
|
||||||
|
<mat-icon matChipRemove>cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Tag Button -->
|
||||||
|
<div class="add-tag-section">
|
||||||
|
<button
|
||||||
|
mat-fab
|
||||||
|
color="primary"
|
||||||
|
class="add-tag-fab"
|
||||||
|
(click)="toggleAddTag()"
|
||||||
|
[matTooltip]="showAddTag ? 'Close tag manager' : 'Add new tag'">
|
||||||
|
<mat-icon>{{ showAddTag ? 'close' : 'add' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Tag Form -->
|
||||||
|
<mat-card class="add-tag-card" *ngIf="showAddTag">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>
|
||||||
|
<mat-icon>new_label</mat-icon>
|
||||||
|
Add New Tag
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<form class="tag-form">
|
||||||
|
<!-- Tag Name Input -->
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Tag Name</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="newTagName"
|
||||||
|
name="tagName"
|
||||||
|
placeholder="Enter tag name"
|
||||||
|
(keyup.enter)="addTag()">
|
||||||
|
<mat-icon matSuffix>label</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Scope Section -->
|
||||||
|
<div class="scope-section">
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
color="accent"
|
||||||
|
type="button"
|
||||||
|
(click)="toggleScopeInput()">
|
||||||
|
<mat-icon>{{ showScopeInput ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||||||
|
{{ showScopeInput ? 'Hide Scope Options' : 'Add Scope' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="scope-inputs" *ngIf="showScopeInput">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Predefined Scope</mat-label>
|
||||||
|
<mat-select [(ngModel)]="newTagScope" name="scopeSelect">
|
||||||
|
<mat-option value="">No scope</mat-option>
|
||||||
|
<mat-option *ngFor="let scope of predefinedScopes" [value]="scope">
|
||||||
|
{{ scope | titlecase }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Custom Scope</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="newTagScope"
|
||||||
|
name="customScope"
|
||||||
|
placeholder="Enter custom scope">
|
||||||
|
<mat-icon matSuffix>category</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
(click)="addTag()"
|
||||||
|
[disabled]="!newTagName.trim()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Add Tag
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
(click)="resetForm()">
|
||||||
|
<mat-icon>clear</mat-icon>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- Available Tags (Quick Add) -->
|
||||||
|
<mat-expansion-panel class="available-tags-panel" *ngIf="showAddTag && availableTags.length > 0">
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
<mat-icon>library_add</mat-icon>
|
||||||
|
Quick Add Existing Tags
|
||||||
|
</mat-panel-title>
|
||||||
|
<mat-panel-description>
|
||||||
|
Click to add existing tags
|
||||||
|
</mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<!-- Unscoped Tags -->
|
||||||
|
<div class="tag-group" *ngIf="getTagsByScope().length > 0">
|
||||||
|
<h4>General Tags</h4>
|
||||||
|
<mat-chip-set class="available-chip-set">
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let tag of getTagsByScope()"
|
||||||
|
class="available-tag"
|
||||||
|
[class.attached]="isTagAttached(tag.name)"
|
||||||
|
[disabled]="isTagAttached(tag.name)"
|
||||||
|
(click)="!isTagAttached(tag.name) && addExistingTag(tag)">
|
||||||
|
{{ tag.name }}
|
||||||
|
<mat-icon *ngIf="isTagAttached(tag.name)" matChipTrailingIcon>check</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scoped Tags -->
|
||||||
|
<div class="tag-group" *ngFor="let scope of getUniqueScopes()">
|
||||||
|
<h4>{{ scope | titlecase }} Tags</h4>
|
||||||
|
<mat-chip-set class="available-chip-set">
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let tag of getTagsByScope(scope)"
|
||||||
|
class="available-tag scoped"
|
||||||
|
[class.attached]="isTagAttached(tag.name)"
|
||||||
|
[disabled]="isTagAttached(tag.name)"
|
||||||
|
(click)="!isTagAttached(tag.name) && addExistingTag(tag)">
|
||||||
|
{{ tag.name }}
|
||||||
|
<mat-icon *ngIf="isTagAttached(tag.name)" matChipTrailingIcon>check</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
.tag-manager {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-tags-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
mat-chip-set {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-tag {
|
||||||
|
background-color: #e3f2fd !important;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
|
||||||
|
mat-icon[matChipAvatar] {
|
||||||
|
background-color: #1976d2 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
.add-tag-fab {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-card {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
mat-card-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
mat-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.scope-inputs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-tags-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.tag-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #424242;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #2196f3;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-chip-set {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-tag {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:not(.attached):hover {
|
||||||
|
background-color: #e8f5e8 !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.attached {
|
||||||
|
background-color: #c8e6c9 !important;
|
||||||
|
color: #2e7d32 !important;
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scoped {
|
||||||
|
border-left: 3px solid #ff9800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material Design overrides
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-chip {
|
||||||
|
--mdc-chip-container-height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.current-tag {
|
||||||
|
--mdc-chip-with-avatar-container-height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-chip-set {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-fab {
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-expansion-panel-header {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-expansion-panel-body {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tag-manager {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-card {
|
||||||
|
margin: 0 8px;
|
||||||
|
|
||||||
|
.tag-form .scope-inputs {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-group {
|
||||||
|
.available-chip-set {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.add-tag-card .tag-form .form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
frontend/src/app/components/tag-manager/tag-manager.component.ts
Normal file
173
frontend/src/app/components/tag-manager/tag-manager.component.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
import { Tag } from '../../models/artifact.interface';
|
||||||
|
import { ArtifactService } from '../../services/artifact.service';
|
||||||
|
import { NotificationService } from '../../services/notification.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tag-manager',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatExpansionModule
|
||||||
|
],
|
||||||
|
templateUrl: './tag-manager.component.html',
|
||||||
|
styleUrl: './tag-manager.component.scss'
|
||||||
|
})
|
||||||
|
export class TagManagerComponent implements OnInit {
|
||||||
|
@Input() artifactId!: number;
|
||||||
|
@Input() currentTags: string[] = [];
|
||||||
|
@Output() tagsUpdated = new EventEmitter<void>();
|
||||||
|
|
||||||
|
availableTags: Tag[] = [];
|
||||||
|
newTagName = '';
|
||||||
|
newTagScope = '';
|
||||||
|
showAddTag = false;
|
||||||
|
showScopeInput = false;
|
||||||
|
|
||||||
|
predefinedScopes = ['project', 'environment', 'priority', 'category', 'status'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private artifactService: ArtifactService,
|
||||||
|
private notificationService: NotificationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadAvailableTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAvailableTags(): void {
|
||||||
|
this.artifactService.getAllTags().subscribe({
|
||||||
|
next: (tags) => {
|
||||||
|
this.availableTags = tags;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
// Tags endpoint not implemented yet - silently ignore
|
||||||
|
if (error.status === 404) {
|
||||||
|
console.log('Tags API not implemented yet');
|
||||||
|
this.availableTags = [];
|
||||||
|
} else {
|
||||||
|
console.error('Error loading tags:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAddTag(): void {
|
||||||
|
this.showAddTag = !this.showAddTag;
|
||||||
|
if (!this.showAddTag) {
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleScopeInput(): void {
|
||||||
|
this.showScopeInput = !this.showScopeInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm(): void {
|
||||||
|
this.newTagName = '';
|
||||||
|
this.newTagScope = '';
|
||||||
|
this.showScopeInput = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTag(): void {
|
||||||
|
if (!this.newTagName.trim()) return;
|
||||||
|
|
||||||
|
const tag: Tag = {
|
||||||
|
name: this.newTagName.trim(),
|
||||||
|
scope: this.newTagScope.trim() || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.artifactService.createTag(tag).subscribe({
|
||||||
|
next: (createdTag) => {
|
||||||
|
this.artifactService.addTag(this.artifactId, createdTag).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.loadAvailableTags();
|
||||||
|
this.tagsUpdated.emit();
|
||||||
|
this.resetForm();
|
||||||
|
this.showAddTag = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error adding tag to artifact:', error);
|
||||||
|
this.notificationService.showError('Error adding tag to artifact: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating tag:', error);
|
||||||
|
this.notificationService.showError('Error creating tag: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(tag: string): void {
|
||||||
|
const tagToRemove = this.availableTags.find(t => t.name === tag);
|
||||||
|
if (!tagToRemove?.id) return;
|
||||||
|
|
||||||
|
this.artifactService.removeTag(this.artifactId, tagToRemove.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.tagsUpdated.emit();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error removing tag:', error);
|
||||||
|
this.notificationService.showError('Error removing tag: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addExistingTag(tag: Tag): void {
|
||||||
|
this.artifactService.addTag(this.artifactId, tag).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.tagsUpdated.emit();
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error adding existing tag:', error);
|
||||||
|
this.notificationService.showError('Error adding tag: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isTagAttached(tagName: string): boolean {
|
||||||
|
return this.currentTags.includes(tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagsByScope(scope?: string): Tag[] {
|
||||||
|
return this.availableTags.filter(tag => tag.scope === scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUniqueScopes(): string[] {
|
||||||
|
const scopes = this.availableTags
|
||||||
|
.map(tag => tag.scope)
|
||||||
|
.filter((scope, index, arr) => scope && arr.indexOf(scope) === index) as string[];
|
||||||
|
return scopes.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagColor(tag: Tag): string {
|
||||||
|
if (tag.color) return tag.color;
|
||||||
|
|
||||||
|
const colors = ['#e0e7ff', '#fef3c7', '#d1fae5', '#fee2e2', '#f3e8ff', '#dbeafe'];
|
||||||
|
const hash = tag.name.split('').reduce((a, b) => {
|
||||||
|
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||||
|
return a & a;
|
||||||
|
}, 0);
|
||||||
|
return colors[Math.abs(hash) % colors.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<mat-card class="upload-card">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>
|
||||||
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
|
Upload Artifact
|
||||||
|
</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
<form (ngSubmit)="uploadArtifact()" #uploadForm="ngForm" class="upload-form">
|
||||||
|
<!-- File Upload Section -->
|
||||||
|
<div class="file-upload-section">
|
||||||
|
<div class="file-input-container">
|
||||||
|
<input
|
||||||
|
#fileInput
|
||||||
|
type="file"
|
||||||
|
id="file"
|
||||||
|
name="file"
|
||||||
|
(change)="onFileSelected($event)"
|
||||||
|
required
|
||||||
|
style="display: none;">
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Select File</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[value]="selectedFile?.name || ''"
|
||||||
|
placeholder="No file selected"
|
||||||
|
readonly>
|
||||||
|
<button mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
type="button"
|
||||||
|
(click)="fileInput.click()"
|
||||||
|
[attr.aria-label]="'Select file'">
|
||||||
|
<mat-icon>folder_open</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-hint>Supported: CSV, JSON, binary files, PCAP</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="selectedFile" class="selected-file-info">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip color="primary">
|
||||||
|
<mat-icon matChipAvatar>description</mat-icon>
|
||||||
|
{{ selectedFile.name }}
|
||||||
|
</mat-chip>
|
||||||
|
<mat-chip color="accent">
|
||||||
|
<mat-icon matChipAvatar>data_usage</mat-icon>
|
||||||
|
{{ selectedFile.size | number }} bytes
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event ID and Test Name -->
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Event ID</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="eventId"
|
||||||
|
[(ngModel)]="formData.eventId"
|
||||||
|
placeholder="e.g., EVENT_001">
|
||||||
|
<mat-icon matSuffix>event</mat-icon>
|
||||||
|
<mat-hint>Groups multiple artifacts under the same event</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Name</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="testName"
|
||||||
|
[(ngModel)]="formData.testName"
|
||||||
|
placeholder="e.g., login_test">
|
||||||
|
<mat-icon matSuffix>quiz</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Suite and Result -->
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Suite</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="testSuite"
|
||||||
|
[(ngModel)]="formData.testSuite"
|
||||||
|
placeholder="e.g., integration">
|
||||||
|
<mat-icon matSuffix>category</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Test Result</mat-label>
|
||||||
|
<mat-select name="testResult" [(ngModel)]="formData.testResult">
|
||||||
|
<mat-option value="">-- Select --</mat-option>
|
||||||
|
<mat-option *ngFor="let result of testResults" [value]="result">
|
||||||
|
<mat-icon>{{ getResultIcon(result) }}</mat-icon>
|
||||||
|
{{ result | titlecase }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
<mat-icon matSuffix>assignment_turned_in</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version and Binaries -->
|
||||||
|
<div class="form-row">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Version</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="version"
|
||||||
|
[(ngModel)]="formData.version"
|
||||||
|
placeholder="e.g., v1.0.0">
|
||||||
|
<mat-icon matSuffix>tag</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Associated Binaries</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="binaries"
|
||||||
|
[(ngModel)]="formData.binaries"
|
||||||
|
placeholder="e.g., app.exe, lib.dll, config.json">
|
||||||
|
<mat-icon matSuffix>code</mat-icon>
|
||||||
|
<mat-hint>Comma-separated list of binaries/files</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Tags</mat-label>
|
||||||
|
<input matInput
|
||||||
|
name="tags"
|
||||||
|
[(ngModel)]="formData.tags"
|
||||||
|
placeholder="e.g., regression, smoke, critical">
|
||||||
|
<mat-icon matSuffix>label</mat-icon>
|
||||||
|
<mat-hint>Comma-separated tags</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<textarea matInput
|
||||||
|
name="description"
|
||||||
|
[(ngModel)]="formData.description"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe this artifact..."></textarea>
|
||||||
|
<mat-icon matSuffix>description</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- JSON Fields -->
|
||||||
|
<div class="json-section">
|
||||||
|
<h3>Advanced Configuration</h3>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Test Config (JSON)</mat-label>
|
||||||
|
<textarea matInput
|
||||||
|
name="testConfig"
|
||||||
|
[(ngModel)]="formData.testConfig"
|
||||||
|
rows="4"
|
||||||
|
placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
|
||||||
|
<mat-icon matSuffix>settings</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Custom Metadata (JSON)</mat-label>
|
||||||
|
<textarea matInput
|
||||||
|
name="customMetadata"
|
||||||
|
[(ngModel)]="formData.customMetadata"
|
||||||
|
rows="4"
|
||||||
|
placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
|
||||||
|
<mat-icon matSuffix>data_object</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Progress -->
|
||||||
|
<mat-progress-bar *ngIf="uploading"
|
||||||
|
mode="indeterminate"
|
||||||
|
class="upload-progress"></mat-progress-bar>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="uploading || !selectedFile"
|
||||||
|
class="upload-button">
|
||||||
|
<mat-icon>{{ uploading ? 'hourglass_empty' : 'cloud_upload' }}</mat-icon>
|
||||||
|
{{ uploading ? 'Uploading...' : 'Upload Artifact' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Status Messages -->
|
||||||
|
<div *ngIf="uploadStatus" class="status-section">
|
||||||
|
<mat-chip-set>
|
||||||
|
<mat-chip [class]="uploadStatusType">
|
||||||
|
<mat-icon matChipAvatar>
|
||||||
|
{{ uploadStatusType === 'success' ? 'check_circle' : 'error' }}
|
||||||
|
</mat-icon>
|
||||||
|
{{ uploadStatus }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-set>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
.upload-card {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.file-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-file-info {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
mat-chip-set {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
padding: 12px 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
mat-chip-set {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background-color: #c8e6c9 !important;
|
||||||
|
color: #2e7d32 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffcdd2 !important;
|
||||||
|
color: #d32f2f !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material Design overrides
|
||||||
|
:host ::ng-deep {
|
||||||
|
.mat-mdc-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-form-field-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-chip {
|
||||||
|
--mdc-chip-container-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-text-field-wrapper {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.upload-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
176
frontend/src/app/components/upload-form/upload-form.component.ts
Normal file
176
frontend/src/app/components/upload-form/upload-form.component.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { Component, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { ArtifactService } from '../../services/artifact.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-upload-form',
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatSnackBarModule
|
||||||
|
],
|
||||||
|
templateUrl: './upload-form.component.html',
|
||||||
|
styleUrl: './upload-form.component.scss'
|
||||||
|
})
|
||||||
|
export class UploadFormComponent {
|
||||||
|
@Output() uploadSuccess = new EventEmitter<void>();
|
||||||
|
|
||||||
|
selectedFile: File | null = null;
|
||||||
|
uploading = false;
|
||||||
|
uploadStatus = '';
|
||||||
|
uploadStatusType: 'success' | 'error' | '' = '';
|
||||||
|
|
||||||
|
formData = {
|
||||||
|
testName: '',
|
||||||
|
testSuite: '',
|
||||||
|
testResult: '',
|
||||||
|
version: '',
|
||||||
|
description: '',
|
||||||
|
tags: '',
|
||||||
|
testConfig: '',
|
||||||
|
customMetadata: '',
|
||||||
|
eventId: '',
|
||||||
|
binaries: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
testResults = ['pass', 'fail', 'skip', 'error'];
|
||||||
|
|
||||||
|
constructor(private artifactService: ArtifactService) {}
|
||||||
|
|
||||||
|
onFileSelected(event: any): void {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
this.selectedFile = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm(): void {
|
||||||
|
this.selectedFile = null;
|
||||||
|
this.formData = {
|
||||||
|
testName: '',
|
||||||
|
testSuite: '',
|
||||||
|
testResult: '',
|
||||||
|
version: '',
|
||||||
|
description: '',
|
||||||
|
tags: '',
|
||||||
|
testConfig: '',
|
||||||
|
customMetadata: '',
|
||||||
|
eventId: '',
|
||||||
|
binaries: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileInput = document.getElementById('file') as HTMLInputElement;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showUploadStatus(message: string, success: boolean): void {
|
||||||
|
this.uploadStatus = message;
|
||||||
|
this.uploadStatusType = success ? 'success' : 'error';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.uploadStatus = '';
|
||||||
|
this.uploadStatusType = '';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadArtifact(): void {
|
||||||
|
if (!this.selectedFile) {
|
||||||
|
this.showUploadStatus('Please select a file to upload', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploading = true;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', this.selectedFile);
|
||||||
|
|
||||||
|
const fields = ['testName', 'testSuite', 'testResult', 'version', 'description', 'eventId'];
|
||||||
|
fields.forEach(field => {
|
||||||
|
const key = field === 'testName' ? 'test_name' :
|
||||||
|
field === 'testSuite' ? 'test_suite' :
|
||||||
|
field === 'testResult' ? 'test_result' :
|
||||||
|
field === 'eventId' ? 'event_id' : field;
|
||||||
|
|
||||||
|
const value = this.formData[field as keyof typeof this.formData];
|
||||||
|
if (value) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.formData.tags) {
|
||||||
|
const tagsArray = this.formData.tags.split(',').map(t => t.trim()).filter(t => t);
|
||||||
|
formData.append('tags', JSON.stringify(tagsArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formData.binaries) {
|
||||||
|
const binariesArray = this.formData.binaries.split(',').map(b => b.trim()).filter(b => b);
|
||||||
|
formData.append('binaries', JSON.stringify(binariesArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formData.testConfig) {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.formData.testConfig);
|
||||||
|
formData.append('test_config', this.formData.testConfig);
|
||||||
|
} catch (e) {
|
||||||
|
this.showUploadStatus('Invalid Test Config JSON', false);
|
||||||
|
this.uploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formData.customMetadata) {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.formData.customMetadata);
|
||||||
|
formData.append('custom_metadata', this.formData.customMetadata);
|
||||||
|
} catch (e) {
|
||||||
|
this.showUploadStatus('Invalid Custom Metadata JSON', false);
|
||||||
|
this.uploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.artifactService.uploadArtifact(formData).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.showUploadStatus(`Successfully uploaded: ${response.filename}`, true);
|
||||||
|
this.resetForm();
|
||||||
|
this.uploadSuccess.emit();
|
||||||
|
this.uploading = false;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
this.showUploadStatus('Upload failed: ' + (error.error?.detail || error.message), false);
|
||||||
|
this.uploading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultIcon(result: string): string {
|
||||||
|
switch (result) {
|
||||||
|
case 'pass': return 'check_circle';
|
||||||
|
case 'fail': return 'cancel';
|
||||||
|
case 'skip': return 'skip_next';
|
||||||
|
case 'error': return 'error';
|
||||||
|
default: return 'help';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,19 +4,18 @@ export interface Artifact {
|
|||||||
file_type: string;
|
file_type: string;
|
||||||
file_size: number;
|
file_size: number;
|
||||||
storage_path: string;
|
storage_path: string;
|
||||||
content_type: string | null;
|
test_name?: string;
|
||||||
test_name: string | null;
|
test_suite?: string;
|
||||||
test_suite: string | null;
|
test_result?: 'pass' | 'fail' | 'skip' | 'error';
|
||||||
test_config: any;
|
test_config?: any;
|
||||||
test_result: string | null;
|
custom_metadata?: any;
|
||||||
sim_source_id: string | null;
|
description?: string;
|
||||||
custom_metadata: any;
|
tags: string[];
|
||||||
description: string | null;
|
version?: string;
|
||||||
tags: string[] | null;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
version: string | null;
|
event_id?: string;
|
||||||
parent_id: number | null;
|
binaries?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtifactQuery {
|
export interface ArtifactQuery {
|
||||||
@@ -25,7 +24,6 @@ export interface ArtifactQuery {
|
|||||||
test_name?: string;
|
test_name?: string;
|
||||||
test_suite?: string;
|
test_suite?: string;
|
||||||
test_result?: string;
|
test_result?: string;
|
||||||
sim_source_id?: string;
|
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
@@ -36,5 +34,18 @@ export interface ArtifactQuery {
|
|||||||
export interface ApiInfo {
|
export interface ApiInfo {
|
||||||
deployment_mode: string;
|
deployment_mode: string;
|
||||||
storage_backend: string;
|
storage_backend: string;
|
||||||
version: string;
|
}
|
||||||
|
|
||||||
|
export interface UploadResponse {
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
scope?: string;
|
||||||
|
color?: string;
|
||||||
|
created_at?: string;
|
||||||
}
|
}
|
||||||
18
frontend/src/app/services/api.service.ts
Normal file
18
frontend/src/app/services/api.service.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiInfo } from '../models/artifact.interface';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ApiService {
|
||||||
|
// Use relative URL - proxy will forward to backend
|
||||||
|
private readonly API_BASE = '';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getApiInfo(): Observable<ApiInfo> {
|
||||||
|
return this.http.get<ApiInfo>(`${this.API_BASE}/api`);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
frontend/src/app/services/artifact.service.ts
Normal file
72
frontend/src/app/services/artifact.service.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
import { Artifact, ArtifactQuery, UploadResponse, Tag } from '../models/artifact.interface';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ArtifactService {
|
||||||
|
// Use relative URL - proxy will forward to backend
|
||||||
|
private readonly API_BASE = '/api/v1';
|
||||||
|
private artifactsSubject = new BehaviorSubject<Artifact[]>([]);
|
||||||
|
public artifacts$ = this.artifactsSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
getArtifacts(limit: number = 25, offset: number = 0): Observable<Artifact[]> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('limit', limit.toString())
|
||||||
|
.set('offset', offset.toString());
|
||||||
|
|
||||||
|
return this.http.get<Artifact[]>(`${this.API_BASE}/artifacts/`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
getArtifact(id: number): Observable<Artifact> {
|
||||||
|
return this.http.get<Artifact>(`${this.API_BASE}/artifacts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadArtifact(formData: FormData): Observable<UploadResponse> {
|
||||||
|
return this.http.post<UploadResponse>(`${this.API_BASE}/artifacts/upload`, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteArtifact(id: number): Observable<any> {
|
||||||
|
return this.http.delete(`${this.API_BASE}/artifacts/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadArtifact(id: number): Observable<Blob> {
|
||||||
|
return this.http.get(`${this.API_BASE}/artifacts/${id}/download`, { responseType: 'blob' });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryArtifacts(query: ArtifactQuery): Observable<Artifact[]> {
|
||||||
|
return this.http.post<Artifact[]>(`${this.API_BASE}/artifacts/query`, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSeedData(count: number): Observable<any> {
|
||||||
|
return this.http.post(`${this.API_BASE}/seed/generate/${count}`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArtifactsCache(artifacts: Artifact[]): void {
|
||||||
|
this.artifactsSubject.next(artifacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentArtifacts(): Artifact[] {
|
||||||
|
return this.artifactsSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTag(artifactId: number, tag: Tag): Observable<any> {
|
||||||
|
return this.http.post(`${this.API_BASE}/artifacts/${artifactId}/tags`, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(artifactId: number, tagId: number): Observable<any> {
|
||||||
|
return this.http.delete(`${this.API_BASE}/artifacts/${artifactId}/tags/${tagId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllTags(): Observable<Tag[]> {
|
||||||
|
return this.http.get<Tag[]>(`${this.API_BASE}/tags`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTag(tag: Tag): Observable<Tag> {
|
||||||
|
return this.http.post<Tag>(`${this.API_BASE}/tags`, tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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}`, {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
frontend/src/app/services/notification.service.ts
Normal file
64
frontend/src/app/services/notification.service.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class NotificationService {
|
||||||
|
|
||||||
|
constructor(private snackBar: MatSnackBar) {}
|
||||||
|
|
||||||
|
showSuccess(message: string, duration: number = 3000): void {
|
||||||
|
this.snackBar.open(message, 'Close', {
|
||||||
|
duration,
|
||||||
|
panelClass: ['success-snackbar'],
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
verticalPosition: 'bottom'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message: string, duration: number = 5000): void {
|
||||||
|
this.snackBar.open(message, 'Close', {
|
||||||
|
duration,
|
||||||
|
panelClass: ['error-snackbar'],
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
verticalPosition: 'bottom'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showInfo(message: string, duration: number = 3000): void {
|
||||||
|
this.snackBar.open(message, 'Close', {
|
||||||
|
duration,
|
||||||
|
panelClass: ['info-snackbar'],
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
verticalPosition: 'bottom'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showWarning(message: string, duration: number = 4000): void {
|
||||||
|
this.snackBar.open(message, 'Close', {
|
||||||
|
duration,
|
||||||
|
panelClass: ['warning-snackbar'],
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
verticalPosition: 'bottom'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfirmation(message: string, action: string = 'Confirm'): Promise<boolean> {
|
||||||
|
const snackBarRef = this.snackBar.open(message, action, {
|
||||||
|
duration: 10000,
|
||||||
|
panelClass: ['confirmation-snackbar'],
|
||||||
|
horizontalPosition: 'center',
|
||||||
|
verticalPosition: 'bottom'
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
snackBarRef.onAction().subscribe(() => resolve(true));
|
||||||
|
snackBarRef.afterDismissed().subscribe((info) => {
|
||||||
|
if (!info.dismissedByAction) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { AppComponent } from './app/app';
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig)
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* Global styles for the Test Artifact Data Lake Angular app with Material Design */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -5,24 +7,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
background: #0f172a;
|
background: linear-gradient(135deg, #3f51b5 0%, #9c27b0 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: #1e293b;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: linear-gradient(135deg, #1e3a8a 0%, #4338ca 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -33,21 +34,6 @@ header {
|
|||||||
header h1 {
|
header h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
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 {
|
.header-info {
|
||||||
@@ -67,8 +53,8 @@ header h1 {
|
|||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #0f172a;
|
background: #f7f9fc;
|
||||||
border-bottom: 2px solid #334155;
|
border-bottom: 2px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -80,22 +66,17 @@ header h1 {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
color: #94a3b8;
|
color: #64748b;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button:hover {
|
.tab-button:hover {
|
||||||
background: #1e293b;
|
background: #e2e8f0;
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button.active {
|
.tab-button.active {
|
||||||
background: #1e293b;
|
background: white;
|
||||||
color: #60a5fa;
|
color: #667eea;
|
||||||
border-bottom: 3px solid #60a5fa;
|
border-bottom: 3px solid #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
@@ -114,52 +95,6 @@ header h1 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-inline {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #0f172a;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
min-width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-inline input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #e2e8f0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-inline input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-inline input::placeholder {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-clear {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #64748b;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-clear:hover {
|
|
||||||
background: #334155;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -168,28 +103,25 @@ header h1 {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: #3b82f6;
|
background: #667eea;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: #2563eb;
|
background: #5568d3;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #334155;
|
background: #e2e8f0;
|
||||||
color: #e2e8f0;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: #475569;
|
background: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@@ -211,9 +143,14 @@ header h1 {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.count-badge {
|
.count-badge {
|
||||||
background: #1e3a8a;
|
background: #f0f9ff;
|
||||||
color: #93c5fd;
|
color: #0369a1;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -223,9 +160,8 @@ header h1 {
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #0f172a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -235,61 +171,30 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
background: #1e293b;
|
background: #f7f9fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #94a3b8;
|
color: #475569;
|
||||||
border-bottom: 2px solid #334155;
|
border-bottom: 2px solid #e2e8f0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.sortable {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.sortable:hover {
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-indicator {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.sort-asc .sort-indicator::after {
|
|
||||||
content: '▲';
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.sort-desc .sort-indicator::after {
|
|
||||||
content: '▼';
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 16px 12px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid #1e293b;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:hover {
|
tbody tr:hover {
|
||||||
background: #1e293b;
|
background: #f7f9fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #64748b;
|
color: #94a3b8;
|
||||||
padding: 40px !important;
|
padding: 40px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,38 +207,68 @@ tbody tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-pass {
|
.result-pass {
|
||||||
background: #064e3b;
|
background: #d1fae5;
|
||||||
color: #6ee7b7;
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-fail {
|
.result-fail {
|
||||||
background: #7f1d1d;
|
background: #fee2e2;
|
||||||
color: #fca5a5;
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-skip {
|
.result-skip {
|
||||||
background: #78350f;
|
background: #fef3c7;
|
||||||
color: #fcd34d;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-error {
|
.result-error {
|
||||||
background: #7f1d1d;
|
background: #fecaca;
|
||||||
color: #fca5a5;
|
color: #7f1d1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #1e3a8a;
|
background: #e0e7ff;
|
||||||
color: #93c5fd;
|
color: #3730a3;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
background: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.removable {
|
||||||
|
background: #f87171;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.removable:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.available {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.available:hover:not(.attached) {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.attached {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-type-badge {
|
.file-type-badge {
|
||||||
background: #1e3a8a;
|
background: #dbeafe;
|
||||||
color: #93c5fd;
|
color: #1e40af;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -352,7 +287,7 @@ tbody tr:hover {
|
|||||||
|
|
||||||
#page-info {
|
#page-info {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #94a3b8;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-section, .query-section {
|
.upload-section, .query-section {
|
||||||
@@ -370,10 +305,16 @@ tbody tr:hover {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #cbd5e1;
|
color: #475569;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -385,49 +326,56 @@ select,
|
|||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
background: #0f172a;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
input:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #3b82f6;
|
border-color: #667eea;
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
color: #64748b;
|
color: #94a3b8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload-status {
|
.upload-status {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload-status.success {
|
.upload-status.success {
|
||||||
background: #064e3b;
|
background: #d1fae5;
|
||||||
color: #6ee7b7;
|
color: #065f46;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload-status.error {
|
.upload-status.error {
|
||||||
background: #7f1d1d;
|
background: #fee2e2;
|
||||||
color: #fca5a5;
|
color: #991b1b;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-file {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -436,7 +384,7 @@ small {
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,15 +395,14 @@ small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: #1e293b;
|
background: white;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid #334155;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
@@ -464,19 +411,19 @@ small {
|
|||||||
top: 20px;
|
top: 20px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #64748b;
|
color: #94a3b8;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close:hover {
|
.close:hover {
|
||||||
color: #e2e8f0;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
border-bottom: 1px solid #334155;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row:last-child {
|
.detail-row:last-child {
|
||||||
@@ -485,29 +432,20 @@ small {
|
|||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #94a3b8;
|
color: #475569;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
color: #cbd5e1;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background: #0f172a;
|
background: #f7f9fc;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border: 1px solid #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: #0f172a;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #93c5fd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@@ -519,68 +457,203 @@ code {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 8px;
|
font-size: 18px;
|
||||||
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.3s;
|
transition: background 0.3s;
|
||||||
color: #94a3b8;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
background: #334155;
|
background: #e2e8f0;
|
||||||
color: #e2e8f0;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure SVG icons inherit color */
|
.filename-link {
|
||||||
.icon-btn svg {
|
color: #667eea;
|
||||||
stroke: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Artifact link styles - softer blue */
|
|
||||||
.artifact-link {
|
|
||||||
color: #93c5fd;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.artifact-link:hover {
|
.filename-link:hover {
|
||||||
color: #bfdbfe;
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clickable row cursor */
|
/* Binaries and Tags cells */
|
||||||
tr.clickable {
|
.binaries-cell, .tags-cell {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binaries-list, .tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-item {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search icon color */
|
.expand-btn:hover {
|
||||||
.search-icon {
|
background: #d1d5db;
|
||||||
color: #64748b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Material Icons */
|
.expanded-binaries, .expanded-tags {
|
||||||
.material-icons {
|
margin-top: 8px;
|
||||||
font-family: 'Material Icons';
|
display: flex;
|
||||||
font-weight: normal;
|
flex-wrap: wrap;
|
||||||
font-style: normal;
|
gap: 4px;
|
||||||
font-size: 20px;
|
}
|
||||||
|
|
||||||
|
/* Tag Manager */
|
||||||
|
.tag-manager {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
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; }
|
.current-tags {
|
||||||
.material-icons.md-18 { font-size: 18px; }
|
display: flex;
|
||||||
.material-icons.md-20 { font-size: 20px; }
|
flex-wrap: wrap;
|
||||||
.material-icons.md-24 { font-size: 24px; }
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-btn:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-btn.active {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-tag-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input, .scope-select, .scope-custom-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-toggle-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-tags {
|
||||||
|
margin-top: 16px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-tags h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-group h5 {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attached-indicator {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter chips */
|
||||||
|
.filter-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-info h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #475569;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.form-row {
|
.form-row {
|
||||||
@@ -601,7 +674,36 @@ tr.clickable {
|
|||||||
padding: 8px 6px;
|
padding: 8px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.binaries-cell, .tags-cell {
|
||||||
flex-wrap: wrap;
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
|
|
||||||
|
/* Material Snackbar Styles */
|
||||||
|
.success-snackbar {
|
||||||
|
background-color: #4caf50 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-snackbar {
|
||||||
|
background-color: #f44336 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-snackbar {
|
||||||
|
background-color: #2196f3 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-snackbar {
|
||||||
|
background-color: #ff9800 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmation-snackbar {
|
||||||
|
background-color: #673ab7 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"include": [
|
"files": [
|
||||||
"src/**/*.ts"
|
"src/main.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"include": [
|
||||||
"src/**/*.spec.ts"
|
"src/**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
@@ -10,25 +11,17 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "preserve"
|
"module": "ES2022"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
"strictInjectionParameters": true,
|
"strictInjectionParameters": true,
|
||||||
"strictInputAccessModifiers": true,
|
"strictInputAccessModifiers": true,
|
||||||
"typeCheckHostBindings": true,
|
|
||||||
"strictTemplates": true
|
"strictTemplates": true
|
||||||
},
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
13
helm/Chart.yaml
Normal file
13
helm/Chart.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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,14 +1,14 @@
|
|||||||
{{/*
|
{{/*
|
||||||
Expand the name of the chart.
|
Expand the name of the chart.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.name" -}}
|
{{- define "datalake.name" -}}
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Create a default fully qualified app name.
|
Create a default fully qualified app name.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.fullname" -}}
|
{{- define "datalake.fullname" -}}
|
||||||
{{- if .Values.fullnameOverride }}
|
{{- if .Values.fullnameOverride }}
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
|
|||||||
{{/*
|
{{/*
|
||||||
Create chart name and version as used by the chart label.
|
Create chart name and version as used by the chart label.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.chart" -}}
|
{{- define "datalake.chart" -}}
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Common labels
|
Common labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.labels" -}}
|
{{- define "datalake.labels" -}}
|
||||||
helm.sh/chart: {{ include "warehouse13.chart" . }}
|
helm.sh/chart: {{ include "datalake.chart" . }}
|
||||||
{{ include "warehouse13.selectorLabels" . }}
|
{{ include "datalake.selectorLabels" . }}
|
||||||
{{- if .Chart.AppVersion }}
|
{{- if .Chart.AppVersion }}
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -43,29 +43,18 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|||||||
{{/*
|
{{/*
|
||||||
Selector labels
|
Selector labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.selectorLabels" -}}
|
{{- define "datalake.selectorLabels" -}}
|
||||||
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
|
app.kubernetes.io/name: {{ include "datalake.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Create the name of the service account to use
|
Create the name of the service account to use
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "warehouse13.serviceAccountName" -}}
|
{{- define "datalake.serviceAccountName" -}}
|
||||||
{{- if .Values.serviceAccount.create }}
|
{{- if .Values.serviceAccount.create }}
|
||||||
{{- default (include "warehouse13.fullname" .) .Values.serviceAccount.name }}
|
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- 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 }}
|
|
||||||
111
helm/templates/deployment.yaml
Normal file
111
helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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 }}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: warehouse13-ingress
|
name: {{ include "datalake.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
{{- include "datalake.labels" . | nindent 4 }}
|
||||||
{{- with .Values.ingress.annotations }}
|
{{- with .Values.ingress.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
@@ -33,9 +33,9 @@ spec:
|
|||||||
pathType: {{ .pathType }}
|
pathType: {{ .pathType }}
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: {{ printf "warehouse13-%s" .backend }}
|
name: {{ include "datalake.fullname" $ }}
|
||||||
port:
|
port:
|
||||||
number: {{ $.Values.app.service.port }}
|
number: {{ $.Values.service.port }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
16
helm/templates/secrets.yaml
Normal file
16
helm/templates/secrets.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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 }}
|
||||||
15
helm/templates/service.yaml
Normal file
15
helm/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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 }}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ServiceAccount
|
kind: ServiceAccount
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "warehouse13.serviceAccountName" . }}
|
name: {{ include "datalake.serviceAccountName" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "warehouse13.labels" . | nindent 4 }}
|
{{- include "datalake.labels" . | nindent 4 }}
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
111
helm/values.yaml
Normal file
111
helm/values.yaml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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"
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 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/
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
|
|
||||||
_ _ _ _ _____
|
|
||||||
| | | | | | / |___ /
|
|
||||||
| | | | __ _ _ __ ___| |__ ___ _ _ ___ / / |_ \
|
|
||||||
| |/\| |/ _` | '__/ _ \ '_ \ / _ \| | | / __|/ / ___) |
|
|
||||||
\ /\ / (_| | | | __/ | | | (_) | |_| \__ \_/ |____/
|
|
||||||
\/ \/ \__,_|_| \___|_| |_|\___/ \__,_|___(_)
|
|
||||||
|
|
||||||
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,97 +0,0 @@
|
|||||||
{{- 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:
|
|
||||||
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 }}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{{- 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 }}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{{- 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 }}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
{{- 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 }}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user