Compare commits

..

20 Commits

Author SHA1 Message Date
pratik
d7390a3a80 switch between arti and default 2025-10-15 14:28:38 -05:00
pratik
4a84c08a2b remove nginx, serve ui from api itself 2025-10-15 14:22:44 -05:00
pratik
d3a14a2b31 Unify quickstart: bring up complete stack (frontend + backend + database) in one command
- Add frontend service to docker-compose.yml (port 4200)
- Simplify quickstart scripts to start all services together
- Remove need for separate dev-start scripts and proxy config
- Frontend now accessible at http://localhost:4200 with nginx proxying API requests
2025-10-15 13:59:03 -05:00
pratik
ed511b1e3c Update styles.scss to match main branch dark theme color scheme 2025-10-15 12:50:27 -05:00
pratik
e81b426d9f Merge main into f/updates - keep Angular Material structure, prepare for styling updates 2025-10-15 12:49:00 -05:00
pratik
b5605d1276 Add .claude/ to gitignore 2025-10-15 12:45:39 -05:00
872464dec2 Add air-gapped deployment option for restricted environments
Added support for air-gapped and enterprise environments where npm package access is restricted, specifically addressing esbuild platform binary download issues.

**New Files:**
- Dockerfile.frontend.prebuilt: Alternative Dockerfile that uses pre-built Angular files
- DEPLOYMENT.md: Comprehensive deployment guide with two options

**Changes:**
- package.json: Added optionalDependencies for esbuild platform binaries
  - @esbuild/darwin-arm64
  - @esbuild/darwin-x64
  - @esbuild/linux-arm64
  - @esbuild/linux-x64

**Deployment Options:**

**Option 1 - Standard Build (current default):**
- Builds Angular in Docker
- Requires npm registry access
- Best for cloud/development

**Option 2 - Pre-built (for air-gapped):**
1. Build Angular locally: npm run build:prod
2. Change dockerfile in docker-compose.yml to Dockerfile.frontend.prebuilt
3. Docker only needs to copy files, no npm required
- No npm registry access needed during Docker build
- Faster, more reliable builds
- Best for enterprise/air-gapped/CI-CD

**Troubleshooting:**
See DEPLOYMENT.md for full troubleshooting guide including:
- esbuild platform binary issues
- Custom npm registry configuration
- Environment-specific recommendations

This addresses npm package access issues in restricted environments while maintaining flexibility for standard deployments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:36:07 -05:00
629e3e4125 Replace Lucide icons with Material Icons for better compatibility
Switched from lucide-angular to Google Material Icons font for better compatibility across all environments, especially air-gapped and enterprise setups.

**Changes:**
- Removed lucide-angular dependency (not available in some environments)
- Added Material Icons font via Google CDN in index.html
- Updated all components to use Material Icons spans instead of Lucide components
- Added Material Icons CSS classes (md-16, md-18, md-20, md-24)

**Icon Mapping:**
- RefreshCw → refresh
- Sparkles → auto_awesome
- Search → search
- X/Close → close
- Download → download
- Trash2/Delete → delete
- Database → storage
- Upload → upload

**Benefits:**
- No npm dependency required (just a font)
- Works in all environments (air-gapped, enterprise proxies)
- Smaller bundle: 349.74 kB raw, 91.98 kB gzipped
- Industry standard Material Design icons
- Better cross-browser compatibility

All components tested and working correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:31:34 -05:00
Patel (US), Pratik
eccdb2f072 Add BSF package lock 2025-10-15 12:26:07 -05:00
0e1a342917 Replace emoji icons with Lucide icons and soften link colors
Replaced emoji icons throughout the Angular app with modern Lucide icon library for a more professional and consistent look matching the original static site design.

**Icon Updates:**
- Navigation tabs: Database, Upload, Search icons
- Toolbar buttons: RefreshCw, Sparkles, Search, X icons
- Action buttons: Download, Trash2 icons
- Form buttons: Upload, Search, X icons

**Style Improvements:**
- Added softer blue color for artifact links (#93c5fd)
- Added hover effect with lighter blue (#bfdbfe)
- Added proper cursor pointer for clickable rows
- Improved icon color consistency throughout

**Dependencies:**
- Added lucide-angular (v0.545.0) for icon support
- Bundle size: 356.54 kB (raw) → 93.91 kB (gzipped)
- Minimal impact: only +7.79 kB for full icon library

All components updated with Lucide imports and icon references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:19:36 -05:00
6f7e9a517e Change frontend port from 80 to 4200 for better compatibility
Changed the frontend container port mapping from 80:80 to 4200:80 to avoid conflicts with system services and improve browser compatibility on macOS.

Port 4200 is the standard Angular development port and is less likely to be blocked by system security settings or conflict with other services.

**Access:**
- Frontend: http://localhost:4200
- API: http://localhost:8000
- MinIO Console: http://localhost:9001

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:14:40 -05:00
5fdefecbd0 Downgrade to Angular 19 and add custom npm registry package-lock regeneration
**Angular Downgrade:**
- Downgraded from Angular 20 to Angular 19 for better stability
- Updated all @angular/* packages to ^19.0.0
- Adjusted TypeScript to ~5.8.0 for Angular 19 compatibility
- Added required outputPath and index to angular.json for Angular 19 build requirements
- Verified production build works successfully

**NPM Registry Enhancements:**
- Updated Dockerfile.frontend to regenerate package-lock.json when custom npm registry is provided
- When NPM_REGISTRY is set to custom URL, the build will:
  1. Configure npm to use the custom registry
  2. Delete existing package-lock.json
  3. Generate new package-lock.json with custom registry URLs
  4. Run npm ci with the new lock file
- Default behavior (npmjs.org) unchanged - uses existing package-lock.json

**Build Verification:**
- Local build tested: ✓
- Docker build tested: ✓
- Bundle size: 348.75 kB raw, 91.73 kB gzipped
- No vulnerabilities found

**Usage:**
```bash
# Default registry (uses existing package-lock.json)
./quickstart.sh

# Custom registry (regenerates package-lock.json)
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/ ./quickstart.sh
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:03:42 -05:00
pratik
2ee203b012 update styling 2025-10-15 12:02:47 -05:00
d5c8bd188b Add custom npm registry/proxy support for frontend builds
Added configurable npm registry support to enable use of custom npm proxies or private registries during Docker builds. This is essential for corporate environments, air-gapped deployments, or when using npm mirrors.

**Changes:**
- Dockerfile.frontend: Added NPM_REGISTRY build argument with conditional configuration
- docker-compose.yml: Pass NPM_REGISTRY from environment to build args
- .env.example: Added NPM_REGISTRY configuration with usage examples

**Usage:**
Set NPM_REGISTRY in .env file or as environment variable:
- Nexus: http://nexus.company.com:8081/repository/npm-proxy/
- Artifactory: https://artifactory.company.com/artifactory/api/npm/npm-remote/
- Verdaccio: http://localhost:4873/
- Default: Leave blank for https://registry.npmjs.org/

**Example:**
```bash
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/ ./quickstart.sh
```

Defaults to official npm registry if not specified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:53:34 -05:00
pratik
ca91fdfa15 Fix text visibility and table alignment in dark theme
- Improve badge colors for better readability (white text on blue background)
- Fix count badge with bright blue background and white text
- Fix type chip visibility with brighter blue color
- Fix filename cell alignment by using inline-flex instead of flex
- Improve header chip text color visibility
- Increase artifact fetch limit from 25 to 1000 to show all artifacts
- Add proper styling for accent buttons in toolbar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:49:00 -05:00
pratik
9733eec2af Add Claude settings to gitignore 2025-10-15 11:44:23 -05:00
pratik
9303f3481b Merge main into f/updates
Resolved conflicts by keeping f/updates changes:
- Keep Angular frontend with dark theme styling
- Keep updated quickstart scripts at root level
- Remove static HTML/JS files (replaced by Angular)
- Keep sim_source_id field implementation
- Merge backend improvements from main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:44:01 -05:00
pratik
fbb1dfa67b Integrate dark theme styling from main branch with Angular Material
- Apply dark blue color scheme (#0f172a, #1e293b, #334155) throughout UI
- Update header with blue gradient and Obsidian branding
- Add missing toolbar buttons: Auto-refresh, Seed Data, Search filter
- Implement action buttons (Download, Delete) in artifacts table
- Add client-side search/filtering functionality
- Update app to support sim_source_id field in database
- Move quickstart scripts to repository root for easier access
- Apply dark theme to tables, tabs, and all Material components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:42:34 -05:00
ed5773893e Migrate frontend to Angular 20 with full Docker support
Implemented a complete Angular 20 migration with modern standalone components architecture and production-ready Docker deployment:

**Frontend Migration:**
- Created Angular 20 application with standalone components (no NgModules)
- Implemented three main components: artifacts-list, upload-form, query-form
- Added TypeScript models and services for type-safe API communication
- Migrated dark theme UI with all existing features
- Configured routing and navigation between views
- Set up development proxy for seamless API integration
- Reactive forms with validation for upload and query functionality
- Auto-refresh artifacts every 5 seconds with RxJS observables
- Client-side sorting, filtering, and search capabilities
- Tags displayed as inline badges, SIM source grouping support

**Docker Integration:**
- Multi-stage Dockerfile for Angular (Node 24 build, nginx Alpine serve)
- nginx configuration for SPA routing and API proxy
- Updated docker-compose.yml with frontend service on port 80
- Health checks for all services
- Production-optimized build with gzip compression and asset caching

**Technical Stack:**
- Angular 20 with standalone components
- TypeScript for type safety
- RxJS for reactive programming
- nginx as reverse proxy
- Multi-stage Docker builds for optimal image size

All features fully functional and tested in Docker environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:35:28 -05:00
pratik
82e8cea256 Switch to angular 2025-10-14 23:32:38 -05:00
124 changed files with 35591 additions and 5290 deletions

View File

@@ -0,0 +1,45 @@
{
"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:*)",
"Bash(git ls-tree:*)",
"mcp__ide__getDiagnostics",
"Read(//c/Users/Pratik/Desktop/code/**)",
"Bash(cat:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git merge:*)",
"Bash(git rm:*)",
"Bash(git checkout:*)",
"Bash(git push:*)",
"Bash(Start-Sleep -Seconds 10)"
],
"deny": [],
"ask": []
}
}

View File

@@ -1,3 +1,4 @@
# Python cache
__pycache__
*.pyc
*.pyo
@@ -5,15 +6,50 @@ __pycache__
.Python
env/
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
# Git
.git
.gitignore
# Documentation
*.md
# IDE files
.vscode
.idea
# Logs
*.log
# OS files
.DS_Store
# Configuration
helm/
.gitlab-ci.yml
docker-compose.yml
# Development files
frontend/.vscode
frontend/src/**/*.spec.ts
# Runtime data
*.pid
*.seed
*.pid.lock
# Coverage
coverage

View File

@@ -29,7 +29,6 @@ API_PORT=8000
MAX_UPLOAD_SIZE=524288000
# NPM Configuration (for frontend build)
# Default: https://registry.npmjs.org/ (public npm registry)
# For restricted environments, set to your custom npm proxy/registry URL
# Example: http://your-nexus-server:8081/repository/npm-proxy/
NPM_REGISTRY=https://registry.npmjs.org/
# Leave blank or set to https://registry.npmjs.org/ for default npm registry
# Set to your custom npm proxy/registry URL if needed (e.g., http://your-nexus-server:8081/repository/npm-proxy/)
NPM_REGISTRY=

View File

@@ -1,26 +0,0 @@
name: build
on:
push:
branches:
- main
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.bitstorm.ca # e.g., docker.io for Docker Hub
username: ${{ secrets.REGISTRY_LOGIN }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: git.bitstorm.ca/bitforge/warehouse13:latest

13
.gitignore vendored
View File

@@ -86,10 +86,11 @@ helm/charts/
tmp/
temp/
*.tmp
.claude/settings.local.json
# Node.js
package-lock.json
**/package-lock.json
# Built static files (generated during Docker build from Angular)
static/
# Node/NPM
frontend/node_modules/
frontend/.angular/
frontend/dist/
# Keep .npmrc but ignore sensitive auth info
# The active .npmrc will be generated from .npmrc.public or .npmrc.artifactory

View File

@@ -1,42 +1,164 @@
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
build_container:
stage: build
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
variables:
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
# Test stage
test:
stage: test
image: python:3.11-slim
before_script:
- mkdir -p /tmp/buildah-storage
- export BUILDAH_ROOT="/tmp/buildah-storage"
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- apt-get update && apt-get install -y gcc postgresql-client
- pip install -r requirements.txt
- pip install pytest pytest-asyncio httpx
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 .
- buildah pull git.bitstorm.ca/bitforge/warehouse13:latest
- buildah tag git.bitstorm.ca/bitforge/warehouse13:latest $IMAGE_NAME
- echo "Pushing $IMAGE_NAME"
- buildah push $IMAGE_NAME
- echo "Running tests..."
- python -m pytest tests/ -v || echo "No tests found, skipping"
only:
- branches
- merge_requests
deploy_helm_charts:
stage: deploy
image:
name: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
parallel:
matrix:
# - ENV: "prod"
# VALUES_FILE: "helm/values-prod.yaml"
# CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
# NAMESPACE: "bsf-services-namespace"
# ONLY: "main"
- ENV: "dev"
VALUES_FILE: "helm/warehouse13/values.yaml"
CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
NAMESPACE: "bsf-services-dev-namespace"
# ONLY: ["branches", "!main"]
# Lint stage
lint:
stage: test
image: python:3.11-slim
before_script:
- pip install flake8 black
script:
- kubectl config use-context $CONTEXT
- echo "Deploy - $CI_REGISTRY_NAME - $CI_COMMIT_REF_SLUG - $CI_COMMIT_SHORT_SHA"
- 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-dev ./helm/warehouse13 --namespace $NAMESPACE -f $VALUES_FILE --set app.image.repository=$CI_REGISTRY_IMAGE --set app.image.tag=$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
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

View File

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

View File

@@ -1,42 +1,49 @@
# Multi-stage build: First stage builds Angular frontend
FROM node:20.11-alpine3.19 AS frontend-build
FROM node:18-alpine as frontend-builder
# Accept npm registry as build argument
ARG NPM_REGISTRY=https://registry.npmjs.org/
# Build argument to select npm registry (public or artifactory)
ARG NPM_REGISTRY=public
ARG ARTIFACTORY_AUTH_TOKEN=""
# Install dependencies for native modules
RUN apk add --no-cache python3 make g++
WORKDIR /frontend
# Copy package files
# Copy package files and registry configs
COPY frontend/package*.json ./
COPY frontend/.npmrc.${NPM_REGISTRY} ./.npmrc
# 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"; \
# If using artifactory and auth token is provided, configure it
RUN if [ "$NPM_REGISTRY" = "artifactory" ] && [ -n "$ARTIFACTORY_AUTH_TOKEN" ]; then \
echo "Configuring Artifactory authentication..."; \
fi
# Install dependencies (ignore package-lock.json if using custom registry)
RUN npm install
# Clean install dependencies
RUN npm ci --force
# Copy source code
COPY frontend/ ./
# Copy frontend source
COPY frontend/src ./src
COPY frontend/public ./public
COPY frontend/angular.json ./
COPY frontend/tsconfig*.json ./
# Build for production
RUN npm run build:prod
# Build the Angular app for production
RUN npm run build
# Second stage: Python backend with Angular frontend
# Second stage: Python backend with Angular static files
FROM python:3.11-alpine
WORKDIR /app
# Install system dependencies for Alpine
# Alpine uses apk instead of apt-get and is lighter/faster
RUN apk add --no-cache \
gcc \
musl-dev \
postgresql-dev \
postgresql-client \
linux-headers
linux-headers \
curl
# Copy requirements and install Python dependencies
COPY requirements.txt .
@@ -48,8 +55,8 @@ COPY utils/ ./utils/
COPY alembic/ ./alembic/
COPY alembic.ini .
# Copy built Angular frontend from first stage to static directory
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
# Copy Angular build from frontend-builder stage
COPY --from=frontend-builder /frontend/dist/frontend/browser ./static
# Create non-root user (Alpine uses adduser instead of useradd)
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
@@ -60,7 +67,7 @@ EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
CMD curl -f http://localhost:8000/health
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

38
Dockerfile.frontend Normal file
View 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 from the browser subdirectory
COPY --from=frontend-builder /frontend/dist/frontend/browser /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;"]

View File

@@ -0,0 +1,15 @@
# Dockerfile for pre-built Angular frontend (air-gapped/restricted environments)
# Build the Angular app locally first: cd frontend && npm run build:prod
# Then use this Dockerfile to package the pre-built files
FROM nginx:alpine
# Copy pre-built Angular app to nginx
COPY frontend/dist/frontend/browser /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

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

114
README.md
View File

@@ -1,4 +1,4 @@
# Warehouse13
# Obsidian
**Enterprise Test Artifact Storage**
@@ -36,32 +36,18 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
## Quick Start
### Standard Deployment (Internet Access)
### One-Command Setup
**Linux/macOS:**
```bash
./quickstart.sh
./scripts/quickstart.sh
```
**Windows (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
1. Clone the repository:
@@ -214,54 +200,35 @@ MINIO_BUCKET_NAME=test-artifacts
### Kubernetes with Helm
**Quick Start:**
1. Build and push Docker image:
```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
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-production.yaml
helm install datalake ./helm \
--set image.repository=your-registry/datalake \
--set image.tag=latest \
--namespace datalake \
--create-namespace
```
**Air-Gapped Deployment:**
3. Access the API:
```bash
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-airgapped.yaml
kubectl port-forward -n datalake svc/datalake 8000:8000
```
**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
All component images are fully configurable in `helm/warehouse13/values.yaml`:
- PostgreSQL image and version
- MinIO image and version
- API image and version
- Frontend image and version
- Resource limits and requests
- Storage backend configuration
- Ingress and TLS settings
- Persistence and storage classes
Edit `helm/values.yaml` to customize:
- Replica count
- Resource limits
- Storage backend (S3 vs MinIO)
- Ingress settings
- PostgreSQL settings
- Autoscaling
### GitLab CI/CD
@@ -302,6 +269,23 @@ Store compiled binaries, test data files, or any binary artifacts with full meta
## Development
### NPM Registry Configuration
The frontend supports working with multiple npm registries (public npm vs corporate Artifactory). See [frontend/README-REGISTRY.md](frontend/README-REGISTRY.md) for detailed instructions.
**Quick switch:**
```bash
cd frontend
# Use public npm (default)
npm run registry:public
npm ci --force
# Use Artifactory
npm run registry:artifactory
npm ci --force
```
### Running Tests
```bash
pytest tests/ -v
@@ -336,6 +320,26 @@ alembic upgrade head
- Verify `MINIO_ENDPOINT` is correct
- 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
[Your License Here]

117
app/api/tags.py Normal file
View 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

View File

@@ -1,7 +1,8 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
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)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@@ -9,7 +10,8 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db():
"""Initialize database tables"""
Base.metadata.create_all(bind=engine)
ArtifactBase.metadata.create_all(bind=engine)
TagBase.metadata.create_all(bind=engine)
def get_db():

View File

@@ -1,13 +1,15 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI
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.seed import router as seed_router
from app.api.tags import router as tags_router
from app.database import init_db
from app.config import settings
import logging
import os
from pathlib import Path
# Configure logging
logging.basicConfig(
@@ -19,8 +21,8 @@ logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI(
title="Warehouse13",
description="Enterprise Test Artifact Storage - API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
title="Test Artifact Data Lake",
description="API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
@@ -38,9 +40,10 @@ app.add_middleware(
# Include routers
app.include_router(artifacts_router)
app.include_router(seed_router)
app.include_router(tags_router)
# Static directory setup
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
# Static files configuration - will be set up after routes
static_dir = Path("/app/static")
@app.on_event("startup")
@@ -57,7 +60,7 @@ async def startup_event():
async def api_root():
"""API root endpoint"""
return {
"message": "Warehouse13 - Enterprise Test Artifact Storage",
"message": "Test Artifact Data Lake API",
"version": "1.0.0",
"docs": "/docs",
"deployment_mode": settings.deployment_mode,
@@ -66,41 +69,17 @@ async def api_root():
@app.get("/")
async def ui_root():
"""Serve the UI"""
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
async def serve_angular_app():
"""Serve Angular app index.html"""
static_dir = Path("/app/static")
if static_dir.exists():
return FileResponse(static_dir / "index.html")
else:
# Fallback if static files not found
return {
"message": "Warehouse13 - Enterprise Test Artifact Storage",
"message": "Test Artifact Data Lake API",
"version": "1.0.0",
"docs": "/docs",
"ui": "UI not found. Serving API only.",
"deployment_mode": settings.deployment_mode,
"storage_backend": settings.storage_backend
}
# Catch-all route for Angular SPA routing - must be last
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve Angular SPA static files and handle client-side routing"""
# Try to serve static file first (JS, CSS, images, etc.)
file_path = os.path.join(static_dir, full_path)
if os.path.exists(file_path) and os.path.isfile(file_path):
return FileResponse(file_path)
# For all other routes (Angular client-side routes), serve index.html
index_path = os.path.join(static_dir, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
else:
return {
"message": "Warehouse13 - Enterprise Test Artifact Storage",
"version": "1.0.0",
"docs": "/docs",
"ui": "UI not found. Serving API only.",
"deployment_mode": settings.deployment_mode,
"storage_backend": settings.storage_backend
}
@@ -112,6 +91,31 @@ async def health_check():
return {"status": "healthy"}
# Catch-all route for Angular client-side routing
# This must be last to not interfere with API routes
@app.get("/{full_path:path}")
async def catch_all(full_path: str):
"""Serve Angular app for all non-API routes (SPA routing)"""
if static_dir.exists():
# Check if the requested path is a file in the static directory
file_path = static_dir / full_path
if file_path.is_file() and file_path.exists():
# Determine media type based on file extension
media_type = None
if file_path.suffix == ".js":
media_type = "application/javascript"
elif file_path.suffix == ".css":
media_type = "text/css"
elif file_path.suffix in [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico"]:
media_type = f"image/{file_path.suffix[1:]}"
return FileResponse(file_path, media_type=media_type)
# Otherwise, serve index.html for client-side routing
index_path = static_dir / "index.html"
if index_path.exists():
return FileResponse(index_path, media_type="text/html")
return {"error": "Static files not found"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(

View File

@@ -20,9 +20,7 @@ class Artifact(Base):
test_suite = Column(String(500), index=True)
test_config = Column(JSON)
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
sim_source_id = Column(String(500), index=True) # SIM source identifier for grouping
# Additional metadata
custom_metadata = Column(JSON)

21
app/models/tag.py Normal file
View 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}')>"

View File

@@ -8,7 +8,7 @@ class ArtifactCreate(BaseModel):
test_suite: Optional[str] = None
test_config: Optional[Dict[str, Any]] = None
test_result: Optional[str] = None
sim_source_id: Optional[str] = None # Groups artifacts from same SIM source
sim_source_id: Optional[str] = None
custom_metadata: Optional[Dict[str, Any]] = None
description: Optional[str] = None
tags: Optional[List[str]] = None

28
app/schemas/tag.py Normal file
View 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

156
dev-start.ps1 Normal file
View File

@@ -0,0 +1,156 @@
[CmdletBinding()]
param(
[switch]$Help
)
$ErrorActionPreference = "Stop"
if ($Help) {
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host "Test Artifact Data Lake - Development Setup" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Usage: .\dev-start.ps1 [OPTIONS]" -ForegroundColor White
Write-Host ""
Write-Host "Options:" -ForegroundColor Yellow
Write-Host " -Help Show this help message" -ForegroundColor White
Write-Host ""
Write-Host "This script starts backend services and frontend development server." -ForegroundColor Green
Write-Host ""
exit 0
}
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host "Test Artifact Data Lake - Development Setup" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
# Check if Node.js is installed
if (-not (Get-Command "node" -ErrorAction SilentlyContinue)) {
Write-Host "Error: Node.js is not installed. Please install Node.js 18+ first." -ForegroundColor Red
Write-Host "Visit: https://nodejs.org/" -ForegroundColor Yellow
Read-Host "Press Enter to exit"
exit 1
}
# Check Node.js version
try {
$nodeVersion = & node --version
$majorVersion = [int]($nodeVersion -replace 'v(\d+)\..*', '$1')
if ($majorVersion -lt 18) {
Write-Host "Error: Node.js version 18 or higher is required. Current version: $nodeVersion" -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
Write-Host "[OK] Node.js version: $nodeVersion" -ForegroundColor Green
}
catch {
Write-Host "Error: Failed to check Node.js version" -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
# Check if npm is installed
if (-not (Get-Command "npm" -ErrorAction SilentlyContinue)) {
Write-Host "Error: npm is not installed. Please install npm first." -ForegroundColor Red
Read-Host "Press Enter to exit"
exit 1
}
$npmVersion = & npm --version
Write-Host "[OK] npm version: $npmVersion" -ForegroundColor Green
# Check if Docker is installed
if (-not (Get-Command "docker" -ErrorAction SilentlyContinue)) {
Write-Host "Error: Docker is not installed. Please install Docker Desktop first." -ForegroundColor Red
Write-Host "Visit: https://www.docker.com/products/docker-desktop" -ForegroundColor Yellow
Read-Host "Press Enter to exit"
exit 1
}
# Check if Docker Compose is available
$ComposeCmd = $null
if (Get-Command "docker-compose" -ErrorAction SilentlyContinue) {
$ComposeCmd = "docker-compose"
} else {
try {
& docker compose version | Out-Null
$ComposeCmd = "docker compose"
}
catch {
Write-Host "Error: Docker Compose is not available." -ForegroundColor Red
Write-Host "Please ensure Docker Desktop is running." -ForegroundColor Yellow
Read-Host "Press Enter to exit"
exit 1
}
}
Write-Host "[OK] Docker Compose command: $ComposeCmd" -ForegroundColor Green
Write-Host ""
# Create .env file if it doesn't exist
if (-not (Test-Path ".env")) {
Write-Host "Creating .env file from .env.example..." -ForegroundColor Yellow
Copy-Item ".env.example" ".env"
Write-Host "[OK] .env file created" -ForegroundColor Green
} else {
Write-Host "[OK] .env file already exists" -ForegroundColor Green
}
Write-Host ""
Write-Host "Starting backend services (PostgreSQL, MinIO, API)..." -ForegroundColor Yellow
# Start backend services
try {
if ($ComposeCmd -eq "docker compose") {
& docker compose up -d postgres minio api
} else {
& docker-compose up -d postgres minio api
}
}
catch {
Write-Host "Error: Failed to start backend services." -ForegroundColor Red
Write-Host "Make sure Docker Desktop is running." -ForegroundColor Yellow
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "Waiting for backend services to be ready..." -ForegroundColor Yellow
Start-Sleep -Seconds 10
Write-Host ""
Write-Host "Installing frontend dependencies..." -ForegroundColor Yellow
Set-Location "frontend"
try {
& npm install
Write-Host "[OK] Frontend dependencies installed" -ForegroundColor Green
}
catch {
Write-Host "Error: Failed to install frontend dependencies" -ForegroundColor Red
Set-Location ".."
Read-Host "Press Enter to exit"
exit 1
}
Write-Host ""
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host "Development Environment Ready!" -ForegroundColor Green
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Backend API: http://localhost:8000" -ForegroundColor White
Write-Host "API Docs: http://localhost:8000/docs" -ForegroundColor White
Write-Host "MinIO Console: http://localhost:9001" -ForegroundColor White
Write-Host " Username: minioadmin" -ForegroundColor Gray
Write-Host " Password: minioadmin" -ForegroundColor Gray
Write-Host ""
Write-Host "Frontend will be available at: http://localhost:4200" -ForegroundColor White
Write-Host ""
Write-Host "To view backend logs: $ComposeCmd logs -f api" -ForegroundColor Yellow
Write-Host "To stop backend: $ComposeCmd down" -ForegroundColor Yellow
Write-Host ""
Write-Host "Starting frontend development server..." -ForegroundColor Green
# Start the frontend development server
& npm run start

89
dev-start.sh Normal file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
set -e
echo "========================================="
echo "Test Artifact Data Lake - Development Setup"
echo "========================================="
echo ""
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed. Please install Node.js 18+ first."
exit 1
fi
# Check Node.js version
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt "18" ]; then
echo "Error: Node.js version 18 or higher is required. Current version: $(node --version)"
exit 1
fi
# Check if npm is installed
if ! command -v npm &> /dev/null; then
echo "Error: npm is not installed. Please install npm first."
exit 1
fi
# Check if Docker is installed for backend services
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
echo "Error: Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
echo "✓ Node.js version: $(node --version)"
echo "✓ npm version: $(npm --version)"
echo "✓ Docker version: $(docker --version)"
echo ""
# Create .env file if it doesn't exist
if [ ! -f .env ]; then
echo "Creating .env file from .env.example..."
cp .env.example .env
echo "✓ .env file created"
else
echo "✓ .env file already exists"
fi
echo ""
echo "Starting backend services (PostgreSQL, MinIO, API)..."
docker-compose up -d postgres minio api
echo ""
echo "Waiting for backend services to be ready..."
sleep 10
echo ""
echo "Installing frontend dependencies..."
cd frontend
npm install
echo ""
echo "========================================="
echo "Development Environment Ready!"
echo "========================================="
echo ""
echo "Backend API: http://localhost:8000"
echo "API Docs: http://localhost:8000/docs"
echo "MinIO Console: http://localhost:9001"
echo " Username: minioadmin"
echo " Password: minioadmin"
echo ""
echo "To start the frontend development server:"
echo " cd frontend"
echo " npm run start"
echo ""
echo "Frontend will be available at: http://localhost:4200"
echo ""
echo "To view backend logs: docker-compose logs -f api"
echo "To stop backend: docker-compose down"
echo ""
echo "Starting frontend development server..."
npm run start

View 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:

View File

@@ -1,7 +1,5 @@
version: '3.8'
name: warehouse13
services:
postgres:
image: postgres:15-alpine
@@ -37,11 +35,7 @@ services:
retries: 5
app:
container_name: warehouse13-app
build:
context: .
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
build: .
ports:
- "8000:8000"
environment:
@@ -58,10 +52,11 @@ services:
minio:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
volumes:
postgres_data:

View File

@@ -24,47 +24,25 @@ This uses `Dockerfile.frontend` which:
---
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments) ⭐ RECOMMENDED
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments)
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access.
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access or when esbuild platform binaries cannot be downloaded.
**Requirements:**
- Node.js 18+ installed locally (on a machine with npm access)
- Node.js 24+ installed locally
- npm installed locally
- No internet required during Docker build
**Note:** This project uses Angular 17 with webpack bundler (not Vite) for better compatibility with restricted npm environments.
**Usage:**
### Quick Start (Recommended)
```bash
./quickstart-airgap.sh
```
This script will:
1. Build the Angular app locally
2. Start all Docker containers
3. Verify the deployment
### Manual Steps
### Step 1: Build Angular app locally
**IMPORTANT:** You MUST run this step BEFORE `docker-compose up`!
```bash
# Option A: Use the helper script
./scripts/build-for-airgap.sh
# Option B: Build manually
cd frontend
npm install # Only needed once or when dependencies change
npm run build:prod
cd ..
```
This creates `frontend/dist/frontend/browser/` which Docker will copy.
### Step 2: Update docker-compose.yml
Edit `docker-compose.yml` and change the frontend dockerfile:
@@ -93,16 +71,26 @@ This uses `Dockerfile.frontend.prebuilt` which:
## Troubleshooting
### Build Tool Package Issues
### esbuild Platform Binary Issues
If you see errors about missing packages like:
If you see errors like:
```
Cannot find package "vite"
Cannot find package "esbuild"
Cannot find package "rollup"
Could not resolve "@esbuild/darwin-arm64"
```
**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.
**Solution 1:** Use Option 2 (Pre-built) above
**Solution 2:** Add platform binaries to package.json (already included):
```json
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.25.4",
"@esbuild/darwin-x64": "^0.25.4",
"@esbuild/linux-arm64": "^0.25.4",
"@esbuild/linux-x64": "^0.25.4"
}
```
**Solution 3:** Use custom npm registry with cached esbuild binaries
### Custom NPM Registry
@@ -121,20 +109,5 @@ NPM_REGISTRY=http://your-proxy ./quickstart.sh
## Recommendation
- **Development/Cloud**: Use Option 1 (standard)
- **Air-gapped/Enterprise**: Use Option 2 (pre-built)**RECOMMENDED**
- **Air-gapped/Enterprise**: Use Option 2 (pre-built)
- **CI/CD**: Use Option 2 for faster, more reliable builds
- **Restricted npm access**: Use Option 2 (pre-built) ⭐ **REQUIRED**
---
## Build Strategy for Restricted Environments
**This project uses Angular 17 with webpack** instead of Angular 19 with Vite specifically for better compatibility with restricted npm environments. Webpack has fewer platform-specific binary dependencies than Vite.
If you encounter any package access errors during builds:
- `Cannot find package "vite"`
- `Cannot find package "rollup"`
- `Cannot find package "esbuild"`
- Any platform-specific binary errors
**Solution:** Use Option 2 (Pre-built) deployment. This completely avoids npm installation in Docker and eliminates all build tool dependency issues.

114
docs/FRONTEND_USAGE.md Normal file
View 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

View File

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

View File

@@ -1,220 +0,0 @@
# NPM Package Age Policy
## Requirement
All npm packages must be **at least 2 weeks old** before they can be used in this project. This ensures:
- Package stability
- Security vulnerability disclosure time
- Compliance with organizational security policies
## How It Works
### 1. Package Version Pinning
The project uses exact version pinning in `package.json` to prevent automatic updates:
```json
{
"dependencies": {
"@angular/core": "19.2.7", // Exact version, not "^19.2.7" or "~19.2.7"
// ...
}
}
```
The `frontend/.npmrc` file enforces this:
```
save-exact=true
package-lock=true
```
### 2. Automated Age Checking
Use the provided script to verify all packages meet the 2-week requirement:
```bash
# Check if all packages are at least 2 weeks old
node scripts/check-package-age.js
```
This script:
- Queries npm registry for publish dates
- Calculates age of each package
- Fails if any package is newer than 14 days
- Shows detailed age information for all packages
### 3. Installation Process
**Always use `npm ci` instead of `npm install`:**
```bash
cd frontend
npm ci # Installs exact versions from package-lock.json
```
**Why `npm ci`?**
- Uses exact versions from `package-lock.json`
- Doesn't update `package-lock.json`
- Ensures reproducible builds
- Faster than `npm install`
## Updating Packages
When you need to add or update packages:
### Step 1: Add Package to package.json
```bash
# Find a version that's at least 2 weeks old
npm view <package-name> time
# Add exact version to package.json
"dependencies": {
"new-package": "1.2.3"
}
```
### Step 2: Verify Age
```bash
node scripts/check-package-age.js
```
### Step 3: Update Lock File
```bash
cd frontend
npm install --package-lock-only
```
### Step 4: Install and Test
```bash
npm ci
npm run build:prod
```
## CI/CD Integration
Add the age check to your CI/CD pipeline:
### GitLab CI Example
```yaml
verify_package_age:
stage: validate
image: node:18-alpine
script:
- node scripts/check-package-age.js
only:
- merge_requests
- main
```
### GitHub Actions Example
```yaml
- name: Check Package Age
run: node scripts/check-package-age.js
```
## Troubleshooting
### "Package is too new" Error
If a package fails the age check:
1. **Find an older version:**
```bash
npm view <package-name> versions --json
npm view <package-name>@<older-version> time
```
2. **Update package.json with older version**
3. **Re-run age check:**
```bash
node scripts/check-package-age.js
```
### Can't Find Old Enough Version
If no version meets the 2-week requirement:
- Wait until the package is at least 2 weeks old
- Look for alternative packages
- Request an exception through your security team
## Example Workflow
```bash
# 1. Check current package ages
node scripts/check-package-age.js
# 2. If all pass, install dependencies
cd frontend
npm ci
# 3. Build application
npm run build:prod
# 4. For air-gapped deployment
../scripts/build-for-airgap.sh
```
## Scripts Reference
| Script | Purpose |
|--------|---------|
| `scripts/check-package-age.js` | Verify all packages are ≥ 2 weeks old |
| `scripts/pin-old-versions.sh` | Helper script to validate and pin versions |
| `scripts/build-for-airgap.sh` | Build frontend for air-gapped deployment |
## Best Practices
1. **Always commit `package-lock.json`**
- Ensures everyone uses the same versions
- Required for reproducible builds
2. **Use `npm ci` in CI/CD**
- Faster than `npm install`
- Enforces lock file versions
- Prevents surprises
3. **Regular audits**
```bash
# Check for security vulnerabilities
npm audit
# Check package ages
node scripts/check-package-age.js
```
4. **Version ranges to avoid**
- ❌ `^1.2.3` (allows minor/patch updates)
- ❌ `~1.2.3` (allows patch updates)
- ❌ `*` or `latest` (allows any version)
- ✅ `1.2.3` (exact version only)
## Package Age Check Output
```
Checking package ages (must be at least 2 weeks old)...
✓ @angular/common@19.2.7 - 45 days old
✓ @angular/compiler@19.2.7 - 45 days old
✓ rxjs@7.8.0 - 180 days old
❌ new-package@1.0.0 - 5 days old (published 2025-01-12)
================================================================================
❌ FAILED: 1 package(s) are newer than 2 weeks:
- new-package@1.0.0 (5 days old, published 2025-01-12)
```
## Support
For questions or exceptions:
- Review with security team
- Document in project README
- Update this policy as needed

195
docs/QUICKSTART.md Normal file
View 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!

View File

@@ -164,7 +164,7 @@ curl -X POST "http://localhost:8000/api/v1/artifacts/query" \
make deploy
# Or directly with Helm
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
helm install datalake ./helm --namespace datalake --create-namespace
```
## Feature Flags Usage
@@ -190,8 +190,9 @@ AWS_REGION=us-east-1
S3_BUCKET_NAME=your-bucket
# Deploy
helm install warehouse13 ./helm/warehouse13 \
--set global.deploymentMode=cloud
helm install datalake ./helm \
--set config.deploymentMode=cloud \
--set aws.enabled=true
```
## What's Next

0
docs/todos.md Normal file
View File

1
frontend/.gitignore vendored
View File

@@ -36,7 +36,6 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
__screenshots__/
# System files
.DS_Store

1
frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmjs.org/

View File

@@ -0,0 +1,11 @@
# Replace YOUR_ARTIFACTORY_URL with your actual Artifactory URL
registry=https://YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/
# If authentication is required, uncomment and configure:
# //YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/:_auth=${ARTIFACTORY_AUTH_TOKEN}
# //YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/:always-auth=true
# Alternative: username/password (less secure, not recommended)
# //YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/:username=${ARTIFACTORY_USERNAME}
# //YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/:_password=${ARTIFACTORY_PASSWORD}
# //YOUR_ARTIFACTORY_URL/artifactory/api/npm/npm-virtual/:email=your-email@company.com

1
frontend/.npmrc.public Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmjs.org/

192
frontend/README-REGISTRY.md Normal file
View File

@@ -0,0 +1,192 @@
# NPM Registry Configuration
This project supports working with two different npm registries:
1. **Public registry** - registry.npmjs.org (default)
2. **Artifactory** - Your corporate Artifactory npm registry
## Quick Start
### Switching Registries Locally
**On Linux/Mac:**
```bash
cd frontend
# Use public npm registry (default)
./switch-registry.sh public
npm ci --force
# Use Artifactory registry
./switch-registry.sh artifactory
# Set auth token if required
export ARTIFACTORY_AUTH_TOKEN="your_token_here"
npm ci --force
```
**On Windows:**
```powershell
cd frontend
# Use public npm registry (default)
.\switch-registry.ps1 public
npm ci --force
# Use Artifactory registry
.\switch-registry.ps1 artifactory
# Set auth token if required
$env:ARTIFACTORY_AUTH_TOKEN = "your_token_here"
npm ci --force
```
### Building with Docker
**Using public npm registry (default):**
```bash
docker compose build app
```
**Using Artifactory registry:**
```bash
# Without authentication
docker compose build app --build-arg NPM_REGISTRY=artifactory
# With authentication
docker compose build app \
--build-arg NPM_REGISTRY=artifactory \
--build-arg ARTIFACTORY_AUTH_TOKEN="your_token_here"
```
**On Windows PowerShell:**
```powershell
# With authentication
docker compose build app `
--build-arg NPM_REGISTRY=artifactory `
--build-arg ARTIFACTORY_AUTH_TOKEN="your_token_here"
```
## Configuration Files
- **`.npmrc.public`** - Configuration for public npm registry
- **`.npmrc.artifactory`** - Configuration for Artifactory registry (edit this with your Artifactory URL)
- **`.npmrc`** - Active configuration (generated by switch-registry scripts)
## Setup Artifactory Configuration
1. Edit `frontend/.npmrc.artifactory` and replace `YOUR_ARTIFACTORY_URL` with your actual Artifactory URL:
```
registry=https://artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/
```
2. If authentication is required, uncomment the auth lines and use one of these methods:
**Method 1: Auth Token (Recommended)**
```
//artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/:_auth=${ARTIFACTORY_AUTH_TOKEN}
//artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/:always-auth=true
```
Then set the environment variable:
```bash
export ARTIFACTORY_AUTH_TOKEN="your_base64_encoded_token"
```
**Method 2: Username/Password**
```
//artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/:username=${ARTIFACTORY_USERNAME}
//artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/:_password=${ARTIFACTORY_PASSWORD}
//artifactory.yourcompany.com/artifactory/api/npm/npm-virtual/:email=your-email@company.com
```
## Handling package-lock.json
The `package-lock.json` file will be different depending on which registry you use. Here are strategies to manage this:
### Strategy 1: Separate Lockfiles (Recommended)
Keep two lockfiles and switch between them:
```bash
# After switching to public and installing
npm ci --force
cp package-lock.json package-lock.public.json
# After switching to artifactory and installing
npm ci --force
cp package-lock.json package-lock.artifactory.json
# When switching registries in the future
cp package-lock.public.json package-lock.json # or
cp package-lock.artifactory.json package-lock.json
```
### Strategy 2: Regenerate Lockfile
Always regenerate the lockfile after switching:
```bash
./switch-registry.sh artifactory
rm package-lock.json
npm install
```
### Strategy 3: Git Ignore Lockfile (Not Recommended for Production)
If you're frequently switching and don't need deterministic builds:
Add to `.gitignore`:
```
frontend/package-lock.json
```
**Warning:** This reduces build reproducibility.
## Troubleshooting
### Issue: "npm ci requires package-lock.json"
**Solution:** Delete `package-lock.json` and run `npm install` to generate a new one for your current registry.
### Issue: "404 Not Found - GET https://registry.npmjs.org/..."
**Solution:** Your .npmrc is pointing to Artifactory but packages don't exist there.
```bash
./switch-registry.sh public
npm ci --force
```
### Issue: "401 Unauthorized"
**Solution:** Check your authentication configuration in `.npmrc.artifactory` and ensure environment variables are set correctly.
### Issue: "ENOENT: no such file or directory, open '.npmrc.public'"
**Solution:** You're missing the registry config files. Make sure both `.npmrc.public` and `.npmrc.artifactory` exist in the frontend directory.
## CI/CD Integration
For CI/CD pipelines, use environment variables to select the registry:
**GitHub Actions Example:**
```yaml
- name: Build with Artifactory
env:
NPM_REGISTRY: artifactory
ARTIFACTORY_AUTH_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }}
run: |
docker compose build app \
--build-arg NPM_REGISTRY=artifactory \
--build-arg ARTIFACTORY_AUTH_TOKEN="${ARTIFACTORY_AUTH_TOKEN}"
```
**GitLab CI Example:**
```yaml
build:
script:
- docker compose build app
--build-arg NPM_REGISTRY=artifactory
--build-arg ARTIFACTORY_AUTH_TOKEN="${ARTIFACTORY_AUTH_TOKEN}"
variables:
NPM_REGISTRY: artifactory
ARTIFACTORY_AUTH_TOKEN: ${CI_ARTIFACTORY_TOKEN}
```
## Best Practices
1. **Never commit credentials** - Use environment variables for tokens/passwords
2. **Document your Artifactory URL** - Update `.npmrc.artifactory` with your team's URL
3. **Keep both config files** - Commit `.npmrc.public` and `.npmrc.artifactory` to git
4. **Use the scripts** - Always use `switch-registry.sh/ps1` instead of manually editing `.npmrc`
5. **Clean installs** - Use `npm ci --force` after switching to ensure a clean dependency tree

View File

@@ -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
- **Angular 20** with standalone components
- **TypeScript** for type safety
- **Reactive Forms** for upload and query functionality
- **RxJS** for reactive programming
- **Auto-refresh** artifacts every 5 seconds
- **Client-side sorting and filtering**
- **Dark theme** UI
- **Responsive design**
**Multi-component Architecture**: Built with reusable Angular components
**Tab Navigation**: Clean tab-based interface for Artifacts, Upload, and Query
**Event ID Support**: Group multiple artifacts under the same event ID
**Expandable Binaries Display**: Show first 4 binaries, expandable for more
**Advanced Tag Management**: Create tags on-the-spot with database persistence
**Scoped Tags**: Organize tags by scope (project, environment, priority, etc.)
**Comprehensive Filtering**: Filter artifacts by all table criteria
**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
### Prerequisites
- Node.js (v18 or later)
- Angular CLI 19
- Node.js 24.x or higher
- npm 11.x or higher
- Backend API running on port 8000
### Installation
### Setup
```bash
cd frontend
npm install
```
### Run Development Server
### Development Server
```bash
npm start
# or
ng serve
```
The application will be available at `http://localhost:4200/`
The development server includes a proxy configuration that forwards `/api` requests to `http://localhost:8000`.
The app will be available at `http://localhost:4200`
### Build for Production
```bash
npm run build:prod
npm run build
# or
ng build
```
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
Built files will be in `dist/frontend/`
## 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
- `GET /api/v1/artifacts/:id` - Get single artifact
- `POST /api/v1/artifacts/upload` - Upload new artifact
- `POST /api/v1/artifacts/query` - Query with filters
- `GET /api/v1/artifacts/:id/download` - Download artifact file
- `DELETE /api/v1/artifacts/:id` - Delete artifact
- `POST /api/v1/seed/generate/:count` - Generate seed data
### Required API Endpoints
- `GET /api` - API information
- `GET /api/v1/artifacts/` - List artifacts
- `GET /api/v1/artifacts/{id}` - Get artifact details
- `POST /api/v1/artifacts/upload` - Upload artifact
- `DELETE /api/v1/artifacts/{id}` - Delete artifact
- `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
{
"/api": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true
}
}
```
### 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.
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:
- Dark blue/slate color palette
- Gradient headers
- Responsive design
- Smooth transitions and hover effects
- Tag badges for categorization
- Result badges for test statuses
Filters are applied immediately as you type, and active filters are displayed as visual chips.
## Browser Support
## Architecture Improvements
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
### From Static to Angular
The original static JavaScript implementation has been converted to:
## 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
2. **Type Safety**: Use TypeScript interfaces in `models/` for all API responses
3. **State Management**: Currently using component-level state; consider NgRx for complex state
4. **Testing**: Run `npm test` for unit tests (Jasmine/Karma)
### Benefits
- **Maintainability**: Clear separation of concerns
- **Reusability**: Components can be reused and extended
- **Testing**: Angular's testing framework support
- **Performance**: Optimized change detection and lazy loading
- **Developer Experience**: Hot reload, TypeScript, and Angular DevTools
## 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
server {
listen 80;
server_name your-domain.com;
root /path/to/dist/frontend/browser;
### Integration with Existing Backend
The Angular frontend is designed to be a drop-in replacement for the static frontend. Simply:
location / {
try_files $uri $uri/ /index.html;
}
1. Build the Angular app: `npm run build`
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 {
proxy_pass http://backend:8000;
}
}
```
## Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
## Future Enhancements
- [ ] Add NgRx for state management
- [ ] Implement WebSocket for real-time updates
- [ ] Add Angular Material components
- [ ] Unit and E2E tests
- [ ] PWA support
- [ ] Drag-and-drop file upload
- [ ] Bulk operations
- [ ] Export to CSV/JSON
## License
Same as parent project
- Drag and drop file upload
- Bulk operations
- Advanced data visualization
- Real-time updates via WebSocket
- Export functionality
- User authentication integration

View File

@@ -0,0 +1,339 @@
# NPM Registry - Usage Examples
## Quick Reference
### Use Public NPM (Default)
```bash
# Linux/Mac
./quickstart.sh
# Windows
.\quickstart.ps1
```
### Use Artifactory
```bash
# Linux/Mac
export ARTIFACTORY_AUTH_TOKEN="your_token_here"
./quickstart.sh -bsf
# Windows
$env:ARTIFACTORY_AUTH_TOKEN = "your_token_here"
.\quickstart.ps1 -Bsf
```
### Rebuild with Artifactory
```bash
# Linux/Mac
export ARTIFACTORY_AUTH_TOKEN="your_token_here"
./quickstart.sh --rebuild -bsf
# Windows
$env:ARTIFACTORY_AUTH_TOKEN = "your_token_here"
.\quickstart.ps1 -Rebuild -Bsf
```
## Local Development (Without Docker)
### Switch Registry for Local Development
**Linux/Mac:**
```bash
cd frontend
# Switch to public npm
./switch-registry.sh public
npm ci --force
npm start
# Switch to Artifactory
./switch-registry.sh artifactory
export ARTIFACTORY_AUTH_TOKEN="your_token"
npm ci --force
npm start
```
**Windows:**
```powershell
cd frontend
# Switch to public npm
.\switch-registry.ps1 public
npm ci --force
npm start
# Switch to Artifactory
.\switch-registry.ps1 artifactory
$env:ARTIFACTORY_AUTH_TOKEN = "your_token"
npm ci --force
npm start
```
**Using NPM Scripts (Cross-platform):**
```bash
cd frontend
# Switch to public npm
npm run registry:public
npm ci --force
npm start
# Switch to Artifactory
npm run registry:artifactory
npm ci --force
npm start
```
## Docker Build Examples
### Build Specific Service with Registry
**Public NPM:**
```bash
docker compose build app
```
**Artifactory:**
```bash
# Without auth
docker compose build app --build-arg NPM_REGISTRY=artifactory
# With auth
docker compose build app \
--build-arg NPM_REGISTRY=artifactory \
--build-arg ARTIFACTORY_AUTH_TOKEN="your_token"
```
**Windows PowerShell:**
```powershell
docker compose build app `
--build-arg NPM_REGISTRY=artifactory `
--build-arg ARTIFACTORY_AUTH_TOKEN="your_token"
```
## Common Workflows
### Corporate Network Development
When working from a corporate network that requires Artifactory:
1. **First time setup:**
```bash
# Edit .npmrc.artifactory with your Artifactory URL
nano frontend/.npmrc.artifactory
# Set auth token (get from your Artifactory admin)
export ARTIFACTORY_AUTH_TOKEN="your_base64_token"
# Start with Artifactory
./quickstart.sh -bsf
```
2. **Daily development:**
```bash
export ARTIFACTORY_AUTH_TOKEN="your_token"
./quickstart.sh -bsf
```
### Home/Public Network Development
When working from home or a network with npm access:
```bash
# Just run without -bsf flag
./quickstart.sh
```
### Switching Between Environments
**Moving from Corporate to Home:**
```bash
# Stop existing containers
docker compose down
# Rebuild with public npm
./quickstart.sh --rebuild
```
**Moving from Home to Corporate:**
```bash
# Stop existing containers
docker compose down
# Rebuild with Artifactory
export ARTIFACTORY_AUTH_TOKEN="your_token"
./quickstart.sh --rebuild -bsf
```
## Handling Multiple package-lock.json Files
### Save lockfiles for both registries:
```bash
cd frontend
# Generate public lockfile
./switch-registry.sh public
rm package-lock.json
npm install
cp package-lock.json package-lock.public.json
# Generate artifactory lockfile
./switch-registry.sh artifactory
rm package-lock.json
npm install
cp package-lock.json package-lock.artifactory.json
# Add to git
git add package-lock.public.json package-lock.artifactory.json
```
### Use the appropriate lockfile:
```bash
# When using public npm
cp package-lock.public.json package-lock.json
npm ci
# When using Artifactory
cp package-lock.artifactory.json package-lock.json
npm ci
```
## CI/CD Examples
### GitHub Actions
**.github/workflows/build.yml**
```yaml
name: Build
on: [push, pull_request]
jobs:
build-public:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build with public npm
run: |
docker compose build app
docker compose up -d
build-artifactory:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build with Artifactory
env:
ARTIFACTORY_AUTH_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }}
run: |
./quickstart.sh -bsf
```
### GitLab CI
**.gitlab-ci.yml**
```yaml
variables:
NPM_REGISTRY: "public"
build:public:
stage: build
script:
- docker compose build app
- docker compose up -d
only:
- main
build:artifactory:
stage: build
variables:
NPM_REGISTRY: "artifactory"
script:
- export ARTIFACTORY_AUTH_TOKEN="${CI_ARTIFACTORY_TOKEN}"
- ./quickstart.sh -bsf
only:
- develop
```
### Jenkins Pipeline
**Jenkinsfile**
```groovy
pipeline {
agent any
environment {
ARTIFACTORY_AUTH_TOKEN = credentials('artifactory-npm-token')
}
stages {
stage('Build with Artifactory') {
steps {
sh './quickstart.sh -bsf'
}
}
}
}
```
## Troubleshooting
### Build fails with "Cannot find .npmrc.public"
**Problem:** Registry config files are missing.
**Solution:**
```bash
cd frontend
# Verify files exist
ls -la .npmrc.*
# If missing, they should be committed to git
git status
```
### "ENOENT: no such file or directory, open '/frontend/dist/frontend/browser'"
**Problem:** Frontend build failed due to registry issues.
**Solution:**
```bash
# Check build logs
docker compose logs app | grep npm
# Try rebuilding with verbose logging
docker compose build app --no-cache --progress=plain
```
### npm ci fails with 404 errors
**Problem:** Wrong registry is configured.
**Solution:**
```bash
cd frontend
cat .npmrc # Check which registry is active
# If using wrong one, switch:
npm run registry:public # or registry:artifactory
npm ci --force
```
### Authentication fails with Artifactory
**Problem:** Token is invalid or not set.
**Solution:**
```bash
# Check token is set
echo $ARTIFACTORY_AUTH_TOKEN # Linux/Mac
echo $env:ARTIFACTORY_AUTH_TOKEN # Windows
# Get new token from Artifactory UI:
# Artifactory -> User Profile -> Generate Token
# Set the token
export ARTIFACTORY_AUTH_TOKEN="your_new_token"
```

View File

@@ -1,17 +1,25 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/frontend",
"index": "src/index.html",
@@ -20,6 +28,7 @@
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
@@ -27,14 +36,13 @@
}
],
"styles": [
"src/styles.css"
]
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"optimization" : {
"fonts": false
},
"budgets": [
{
"type": "initial",
@@ -58,7 +66,10 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
"buildTarget": "frontend:build:production"
@@ -70,16 +81,17 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
@@ -87,14 +99,13 @@
}
],
"styles": [
"src/styles.css"
]
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}

13838
frontend/package-lock-bsf.json Normal file

File diff suppressed because it is too large Load Diff

15273
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,81 +3,39 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
"test": "ng test",
"registry:public": "node -e \"require('fs').copyFileSync('.npmrc.public', '.npmrc'); console.log('✓ Switched to public npm registry');\"",
"registry:artifactory": "node -e \"require('fs').copyFileSync('.npmrc.artifactory', '.npmrc'); console.log('✓ Switched to Artifactory registry');\""
},
"private": true,
"dependencies": {
"@angular/common": "19.2.x",
"@angular/compiler": "19.2.x",
"@angular/core": "19.2.x",
"@angular/forms": "19.2.x",
"@angular/platform-browser": "19.2.x",
"@angular/router": "19.2.x",
"@angular/cdk": "^19.2.19",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@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",
"tslib": "^2.8.1",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "19.2.x",
"@angular/cli": "19.2.x",
"@angular/compiler-cli": "19.2.x",
"@angular-devkit/build-angular": "^19.2.17",
"@angular/cli": "^19.2.17",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.9.0",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "5.x.x",
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
},
"resolutions": {
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
},
"overrides": {
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
"typescript": "~5.7.2"
}
}
}

View File

@@ -2,6 +2,10 @@
"/api": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": "/api"
}
}
}

View File

@@ -0,0 +1,152 @@
<div class="app-container">
<!-- Material Toolbar Header -->
<mat-toolbar color="primary" class="app-toolbar">
<mat-icon class="app-icon">diamond</mat-icon>
<span class="app-title">◆ Obsidian</span>
<span class="spacer"></span>
<div class="header-info" *ngIf="apiInfo">
<mat-chip-set>
<mat-chip>
<mat-icon matChipAvatar>settings</mat-icon>
Mode: {{ apiInfo.deployment_mode }}
</mat-chip>
<mat-chip>
<mat-icon matChipAvatar>folder</mat-icon>
Storage: {{ 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="toolbar">
<button mat-raised-button color="primary" (click)="loadArtifacts()">
<mat-icon>refresh</mat-icon>
Refresh
</button>
<button mat-raised-button [color]="autoRefreshEnabled ? 'accent' : ''" (click)="toggleAutoRefresh()">
Auto-refresh: {{ autoRefreshEnabled ? 'ON' : 'OFF' }}
</button>
<button mat-raised-button (click)="generateSeedData()">
<mat-icon>auto_awesome</mat-icon>
Generate Seed Data
</button>
<span class="count-badge">{{ artifacts.length }} artifacts</span>
<mat-form-field appearance="outline" class="filter-search">
<mat-label>Search</mat-label>
<input matInput [(ngModel)]="searchTerm" (input)="filterTable()" placeholder="Search...">
<mat-icon matPrefix>search</mat-icon>
<button mat-icon-button matSuffix *ngIf="searchTerm" (click)="clearSearch()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<table mat-table [dataSource]="filteredArtifacts" 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>
<!-- 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">
<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>
<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>

View File

@@ -0,0 +1,409 @@
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
background: #1e293b;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.app-toolbar {
position: sticky;
top: 0;
z-index: 10;
background: linear-gradient(135deg, #1e3a8a 0%, #4338ca 100%) !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.app-icon {
margin-right: 12px;
}
.app-title {
font-size: 20px;
font-weight: 500;
letter-spacing: 0.5px;
}
.spacer {
flex: 1 1 auto;
}
.header-info {
::ng-deep {
mat-chip-set {
mat-chip {
background-color: rgba(255, 255, 255, 0.2) !important;
.mdc-evolution-chip__action {
color: white !important;
}
.mdc-evolution-chip__text-label {
color: white !important;
}
mat-icon {
color: white !important;
}
}
}
}
}
// Tab Group Styling - Dark Theme
.main-tabs {
flex: 1;
display: flex;
flex-direction: column;
background: #1e293b;
::ng-deep {
.mat-mdc-tab-body-wrapper {
flex: 1;
padding: 0;
background: #1e293b;
}
.mat-mdc-tab-header {
background: #0f172a;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
border-bottom: 2px solid #334155;
}
.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;
.mdc-tab__text-label {
color: #cbd5e1;
}
&:hover {
background: #1e293b;
.mdc-tab__text-label {
color: #e2e8f0;
}
}
}
.mat-mdc-tab.mdc-tab--active {
.mdc-tab__text-label {
color: #60a5fa;
font-weight: 600;
}
}
.mat-mdc-tab-indicator {
.mdc-tab-indicator__content--underline {
background-color: #60a5fa;
height: 3px;
}
}
}
}
// Tab Content Wrapper - Dark Theme
.tab-content-wrapper {
padding: 30px;
max-width: 1400px;
margin: 0 auto;
width: 100%;
min-height: calc(100vh - 180px);
background: #1e293b;
}
// Content Header - Dark Theme
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
h2 {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
font-size: 24px;
font-weight: 500;
color: #e2e8f0;
mat-icon {
color: #60a5fa;
font-size: 28px;
width: 28px;
height: 28px;
}
}
button {
mat-icon {
margin-right: 8px;
}
}
}
// Toolbar styling
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
// Ensure buttons have proper styling
button {
&[color="accent"] {
background-color: #10b981 !important;
color: white !important;
}
}
}
.count-badge {
background: #3b82f6;
color: #ffffff;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
margin-left: auto;
}
// Filter Search Styling
.filter-search {
min-width: 250px;
::ng-deep {
.mat-mdc-text-field-wrapper {
background: #0f172a;
border-radius: 6px;
}
.mat-mdc-form-field-input-control {
color: #e2e8f0;
}
.mat-mdc-form-field-label {
color: #94a3b8;
}
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: #334155 !important;
}
.mat-mdc-form-field:hover .mdc-notched-outline__leading,
.mat-mdc-form-field:hover .mdc-notched-outline__notch,
.mat-mdc-form-field:hover .mdc-notched-outline__trailing {
border-color: #60a5fa !important;
}
.mat-mdc-form-field-icon-prefix,
.mat-mdc-form-field-icon-suffix {
color: #64748b;
}
}
}
// Action buttons styling
.action-buttons {
display: flex;
gap: 4px;
button {
color: #94a3b8;
&:hover {
color: #e2e8f0;
background: #334155;
}
}
}
// Artifacts Table Styling - Dark Theme
.artifacts-table {
width: 100%;
background: #0f172a;
border-radius: 8px;
overflow: hidden;
border: 1px solid #334155;
th.mat-mdc-header-cell {
background: #1e293b;
color: #94a3b8;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 14px 12px;
border-bottom: 2px solid #334155;
}
td.mat-mdc-cell {
padding: 16px 12px;
font-size: 14px;
color: #cbd5e1;
border-bottom: 1px solid #1e293b;
}
tr.mat-mdc-row {
transition: background-color 0.2s ease;
background: #0f172a;
&:hover {
background-color: #1e293b;
}
}
td.filename-cell {
font-weight: 500;
mat-icon {
color: #60a5fa;
font-size: 20px;
width: 20px;
height: 20px;
vertical-align: middle;
margin-right: 8px;
}
}
.type-chip {
background-color: #3b82f6 !important;
color: #ffffff !important;
font-weight: 600;
font-size: 11px;
padding: 4px 12px;
height: auto;
}
.text-muted {
color: #64748b;
}
}
// Result Chips - Material Design style
mat-chip.result-pass {
--mdc-chip-elevated-container-color: #4caf50 !important;
--mdc-chip-label-text-color: #ffffff !important;
background-color: #4caf50 !important;
.mdc-evolution-chip__action {
color: #ffffff !important;
}
.mdc-evolution-chip__text-label {
color: #ffffff !important;
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
}
}
mat-chip.result-fail {
--mdc-chip-elevated-container-color: #f44336 !important;
--mdc-chip-label-text-color: #ffffff !important;
background-color: #f44336 !important;
.mdc-evolution-chip__action {
color: #ffffff !important;
}
.mdc-evolution-chip__text-label {
color: #ffffff !important;
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
}
}
mat-chip.result-skip {
--mdc-chip-elevated-container-color: #ff9800 !important;
--mdc-chip-label-text-color: #ffffff !important;
background-color: #ff9800 !important;
.mdc-evolution-chip__action {
color: #ffffff !important;
}
.mdc-evolution-chip__text-label {
color: #ffffff !important;
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
}
}
mat-chip.result-error {
--mdc-chip-elevated-container-color: #e91e63 !important;
--mdc-chip-label-text-color: #ffffff !important;
background-color: #e91e63 !important;
.mdc-evolution-chip__action {
color: #ffffff !important;
}
.mdc-evolution-chip__text-label {
color: #ffffff !important;
font-weight: 500;
font-size: 12px;
text-transform: uppercase;
}
}
// 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;
}
}
}

View File

@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
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', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
});
});

View File

@@ -0,0 +1,215 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
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 { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTooltipModule } from '@angular/material/tooltip';
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,
FormsModule,
MatToolbarModule,
MatTableModule,
MatTabsModule,
MatChipsModule,
MatIconModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatTooltipModule,
UploadFormComponent,
QueryFormComponent
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, OnDestroy {
title = 'Obsidian - Test Artifact Data Lake';
apiInfo: ApiInfo | null = null;
artifacts: Artifact[] = [];
filteredArtifacts: Artifact[] = [];
displayedColumns: string[] = ['id', 'filename', 'file_type', 'file_size', 'test_name', 'test_result', 'actions'];
selectedTabIndex = 0;
searchTerm: string = '';
autoRefreshEnabled: boolean = true;
private autoRefreshInterval: any;
constructor(
private apiService: ApiService,
private artifactService: ArtifactService
) {}
ngOnInit(): void {
this.loadApiInfo();
this.loadArtifacts();
this.startAutoRefresh();
}
ngOnDestroy(): void {
this.stopAutoRefresh();
}
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;
this.filterTable();
},
error: (error) => {
console.error('Error loading artifacts:', error);
}
});
}
filterTable(): void {
if (!this.searchTerm) {
this.filteredArtifacts = this.artifacts;
return;
}
const term = this.searchTerm.toLowerCase();
this.filteredArtifacts = this.artifacts.filter(artifact => {
return (
artifact.filename?.toLowerCase().includes(term) ||
artifact.test_name?.toLowerCase().includes(term) ||
artifact.test_suite?.toLowerCase().includes(term) ||
artifact.file_type?.toLowerCase().includes(term)
);
});
}
clearSearch(): void {
this.searchTerm = '';
this.filterTable();
}
toggleAutoRefresh(): void {
this.autoRefreshEnabled = !this.autoRefreshEnabled;
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
}
startAutoRefresh(): void {
this.stopAutoRefresh();
if (this.autoRefreshEnabled) {
this.autoRefreshInterval = setInterval(() => {
if (this.selectedTabIndex === 0) {
this.loadArtifacts();
}
}, 5000);
}
}
stopAutoRefresh(): void {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
}
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) {
alert('Please enter a number between 1 and 100');
return;
}
this.artifactService.generateSeedData(num).subscribe({
next: (result: any) => {
alert(result.message || `Successfully generated ${num} artifacts`);
this.loadArtifacts();
},
error: (error) => {
alert('Error generating seed data: ' + error.message);
}
});
}
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) => {
alert('Error downloading artifact: ' + error.message);
}
});
}
deleteArtifact(artifact: Artifact): void {
if (!confirm(`Are you sure you want to delete "${artifact.filename}"? This cannot be undone.`)) {
return;
}
this.artifactService.deleteArtifact(artifact.id).subscribe({
next: () => {
alert('Artifact deleted successfully');
this.loadArtifacts();
},
error: (error) => {
alert('Error deleting artifact: ' + error.message);
}
});
}
onUploadSuccess(): void {
this.loadArtifacts();
this.selectedTabIndex = 0;
}
onQueryResults(artifacts: Artifact[]): void {
this.artifacts = artifacts;
this.filterTable();
this.selectedTabIndex = 0;
}
onFiltersChange(filters: any): void {
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];
}
}

View File

@@ -1,134 +0,0 @@
/* App Layout */
.app-layout {
display: flex;
min-height: 100vh;
background: #0f172a;
color: #e2e8f0;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 280px;
transition: margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.main-content.sidebar-collapsed {
margin-right: 80px;
}
/* Top Header */
.top-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background: #1e293b;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.top-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 600;
color: #f1f5f9;
margin: 0;
}
.logo {
font-family: 'Courier New', monospace;
color: #3b82f6;
font-weight: 700;
letter-spacing: -1px;
}
.header-info {
display: flex;
gap: 12px;
align-items: center;
}
.badge {
padding: 6px 14px;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Content Area */
.content-area {
flex: 1;
padding: 24px;
overflow-y: auto;
}
/* Responsive Adjustments */
@media (max-width: 1024px) {
.main-content {
margin-right: 240px;
}
}
@media (max-width: 768px) {
.main-content {
margin-right: 0;
}
.top-header {
padding: 12px 16px;
}
.top-header h1 {
font-size: 18px;
}
.content-area {
padding: 16px;
}
.header-info {
flex-direction: column;
gap: 6px;
align-items: flex-end;
}
}
/* Smooth Scrolling */
.content-area::-webkit-scrollbar {
width: 8px;
}
.content-area::-webkit-scrollbar-thumb {
background: rgba(59, 130, 246, 0.3);
border-radius: 4px;
}
.content-area::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
/* Global Material Icons Sizing */
.material-icons.md-16 {
font-size: 16px;
}
.material-icons.md-18 {
font-size: 18px;
}
.material-icons.md-20 {
font-size: 20px;
}
.material-icons.md-24 {
font-size: 24px;
}

View File

@@ -1,15 +1,3 @@
import { Routes } from '@angular/router';
import { ArtifactsListComponent } from './components/artifacts-list/artifacts-list';
import { UploadFormComponent } from './components/upload-form/upload-form';
import { QueryFormComponent } from './components/query-form/query-form';
import { SettingsComponent } from './components/settings/settings';
import { ProfileComponent } from './components/profile/profile';
export const routes: Routes = [
{ path: '', redirectTo: '/artifacts', pathMatch: 'full' },
{ path: 'artifacts', component: ArtifactsListComponent },
{ path: 'upload', component: UploadFormComponent },
{ path: 'query', component: QueryFormComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'profile', component: ProfileComponent }
];
export const routes: Routes = [];

View File

@@ -1,30 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { ArtifactService } from './services/artifact';
import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, NavSidebarComponent],
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="app-layout">
<app-nav-sidebar (sidebarToggled)="onSidebarToggle($event)"></app-nav-sidebar>
<main class="main-content" [class.sidebar-collapsed]="isSidebarCollapsed">
<header class="top-header">
<h1><span class="logo">[W13]</span> Warehouse13</h1>
<div class="header-info">
<span class="badge">{{ deploymentMode }}</span>
<span class="badge">{{ storageBackend }}</span>
</div>
</header>
<div class="content-area">
<router-outlet></router-outlet>
<div class="container">
<header>
<h1>◆ Obsidian</h1>
<div class="header-info">
<span class="badge">{{ deploymentMode }}</span>
<span class="badge">{{ storageBackend }}</span>
</div>
</main>
</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']
@@ -32,7 +38,6 @@ import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
export class AppComponent implements OnInit {
deploymentMode: string = '';
storageBackend: string = '';
isSidebarCollapsed: boolean = false;
constructor(private artifactService: ArtifactService) {}
@@ -45,8 +50,4 @@ export class AppComponent implements OnInit {
error: (err) => console.error('Failed to load API info:', err)
});
}
onSidebarToggle(isCollapsed: boolean) {
this.isSidebarCollapsed = isCollapsed;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,276 +0,0 @@
.sidebar {
position: fixed;
right: 0;
top: 0;
height: 100vh;
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 280px;
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.sidebar.collapsed {
width: 80px;
}
/* Toggle Button */
.toggle-btn {
position: absolute;
left: -18px;
top: 20px;
width: 36px;
height: 36px;
border-radius: 50%;
background: #3b82f6;
border: 2px solid #0f172a;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.5);
transition: all 0.2s ease;
z-index: 1001;
overflow: visible;
}
.toggle-btn:hover {
background: #2563eb;
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.6);
}
.toggle-btn .material-icons {
font-size: 22px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* User Section */
.user-section {
padding: 24px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-top: 40px;
}
.sidebar.collapsed .user-section {
padding: 24px 8px;
}
.user-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s ease;
}
.sidebar.collapsed .user-avatar {
width: 48px;
height: 48px;
}
.avatar-text {
font-size: 20px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.user-info {
text-align: center;
width: 100%;
overflow: hidden;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-email {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 11px;
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
padding: 4px 12px;
border-radius: 12px;
display: inline-block;
}
/* Navigation Items */
.nav-items {
flex: 1;
padding: 16px 8px;
overflow-y: auto;
overflow-x: hidden;
}
.nav-items::-webkit-scrollbar {
width: 6px;
}
.nav-items::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.nav-items::-webkit-scrollbar-track {
background: transparent;
}
.nav-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 8px;
color: #cbd5e1;
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.sidebar.collapsed .nav-item {
justify-content: center;
padding: 12px 8px;
}
.nav-item:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.nav-item.active {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background: #3b82f6;
border-radius: 0 4px 4px 0;
}
.nav-icon {
font-size: 24px;
flex-shrink: 0;
}
.nav-label {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar.collapsed .nav-label {
display: none;
}
/* Footer */
.sidebar-footer {
padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.app-info {
text-align: center;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.app-version {
font-size: 11px;
color: #64748b;
}
/* Responsive Design */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.sidebar.collapsed {
width: 70px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
transform: translateX(100%);
}
.sidebar:not(.collapsed) {
transform: translateX(0);
}
.sidebar.collapsed {
width: 60px;
transform: translateX(0);
}
.toggle-btn {
left: auto;
right: 16px;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.nav-label,
.user-info {
animation: slideIn 0.3s ease;
}

View File

@@ -1,40 +0,0 @@
<aside class="sidebar" [class.collapsed]="isCollapsed">
<!-- Toggle Button -->
<button class="toggle-btn" (click)="toggleSidebar()" aria-label="Toggle sidebar">
<span class="material-icons">{{ isCollapsed ? 'chevron_right' : 'chevron_left' }}</span>
</button>
<!-- User Profile Section -->
<div class="user-section">
<div class="user-avatar">
<span class="avatar-text">{{ user.avatar }}</span>
</div>
<div class="user-info" *ngIf="!isCollapsed">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
<div class="user-role">{{ user.role }}</div>
</div>
</div>
<!-- Navigation Items -->
<nav class="nav-items">
<a
*ngFor="let item of navItems"
[routerLink]="item.route"
routerLinkActive="active"
class="nav-item"
[attr.aria-label]="item.label"
>
<span class="material-icons nav-icon">{{ item.icon }}</span>
<span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
</a>
</nav>
<!-- App Info Footer -->
<div class="sidebar-footer" *ngIf="!isCollapsed">
<div class="app-info">
<div class="app-name">Warehouse13</div>
<div class="app-version">v1.0.0</div>
</div>
</div>
</aside>

View File

@@ -1,43 +0,0 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router';
interface NavItem {
route: string;
label: string;
icon: string;
}
@Component({
selector: 'app-nav-sidebar',
standalone: true,
imports: [CommonModule, RouterLink, RouterLinkActive],
templateUrl: './nav-sidebar.html',
styleUrls: ['./nav-sidebar.css']
})
export class NavSidebarComponent {
isCollapsed = false;
@Output() sidebarToggled = new EventEmitter<boolean>();
// Hardcoded user data for now (will be replaced with OAuth)
user = {
name: 'John Doe',
email: 'john.doe@warehouse13.com',
avatar: 'JD',
role: 'Administrator'
};
navItems: NavItem[] = [
{ route: '/artifacts', label: 'Artifacts', icon: 'inventory_2' },
{ route: '/upload', label: 'Upload', icon: 'cloud_upload' },
{ route: '/query', label: 'Query', icon: 'search' },
{ route: '/profile', label: 'Profile', icon: 'person' },
{ route: '/settings', label: 'Settings', icon: 'settings' }
];
toggleSidebar() {
this.isCollapsed = !this.isCollapsed;
this.sidebarToggled.emit(this.isCollapsed);
}
}

View File

@@ -1,395 +0,0 @@
.profile-container {
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.page-header h1 .material-icons {
font-size: 32px;
color: #3b82f6;
}
.subtitle {
color: #94a3b8;
margin: 0;
}
.profile-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Profile Card */
.profile-card {
background: #1e293b;
border-radius: 12px;
padding: 32px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.profile-header {
display: flex;
align-items: flex-start;
gap: 24px;
margin-bottom: 32px;
}
.profile-avatar-large {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4px 24px rgba(59, 130, 246, 0.3);
}
.avatar-text {
font-size: 36px;
font-weight: 600;
color: white;
letter-spacing: 2px;
}
.profile-info {
flex: 1;
}
.profile-info h2 {
font-size: 24px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.email {
color: #94a3b8;
margin: 0 0 12px 0;
font-size: 14px;
}
.profile-meta {
display: flex;
align-items: center;
gap: 16px;
}
.badge-role {
padding: 6px 14px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.department {
display: flex;
align-items: center;
gap: 6px;
color: #cbd5e1;
font-size: 13px;
}
.department .material-icons {
font-size: 18px;
}
.btn-edit {
padding: 10px 20px;
border-radius: 8px;
border: 1px solid #3b82f6;
background: transparent;
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.btn-edit:hover {
background: rgba(59, 130, 246, 0.1);
}
.profile-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-item {
display: flex;
align-items: flex-start;
gap: 12px;
}
.detail-item > .material-icons {
font-size: 20px;
color: #64748b;
margin-top: 2px;
}
.detail-item label {
display: block;
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.detail-item span {
display: block;
font-size: 14px;
color: #f1f5f9;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.stat-card {
background: #1e293b;
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.stat-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.stat-icon {
font-size: 36px;
color: #3b82f6;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #94a3b8;
}
/* Activity Section */
.activity-section,
.security-section {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.activity-section h2,
.security-section h2 {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.activity-section h2 .material-icons,
.security-section h2 .material-icons {
font-size: 24px;
color: #3b82f6;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
background: rgba(15, 23, 42, 0.5);
border-radius: 8px;
transition: all 0.2s ease;
}
.activity-item:hover {
background: rgba(59, 130, 246, 0.05);
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: rgba(59, 130, 246, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.activity-icon .material-icons {
font-size: 20px;
color: #3b82f6;
}
.activity-details {
flex: 1;
}
.activity-action {
font-size: 14px;
color: #f1f5f9;
margin-bottom: 4px;
}
.activity-file {
font-size: 12px;
color: #94a3b8;
}
.activity-time {
font-size: 12px;
color: #64748b;
white-space: nowrap;
}
/* Security Section */
.security-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.security-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: rgba(15, 23, 42, 0.5);
border-radius: 8px;
}
.security-info {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
}
.security-info > .material-icons {
font-size: 24px;
color: #64748b;
margin-top: 2px;
}
.security-info h3 {
font-size: 15px;
font-weight: 500;
color: #f1f5f9;
margin: 0 0 4px 0;
}
.security-info p {
font-size: 13px;
color: #94a3b8;
margin: 0;
}
.btn-secondary {
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.btn-secondary:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
/* Responsive */
@media (max-width: 768px) {
.profile-container {
padding: 16px;
}
.profile-header {
flex-direction: column;
text-align: center;
}
.profile-avatar-large {
margin: 0 auto;
}
.profile-info {
text-align: center;
}
.profile-meta {
justify-content: center;
}
.btn-edit {
width: 100%;
justify-content: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
.security-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.btn-secondary {
width: 100%;
}
}

View File

@@ -1,128 +0,0 @@
<div class="profile-container">
<header class="page-header">
<h1>
<span class="material-icons">person</span>
Profile
</h1>
<p class="subtitle">Manage your account information</p>
</header>
<div class="profile-content">
<!-- User Info Card -->
<section class="profile-card">
<div class="profile-header">
<div class="profile-avatar-large">
<span class="avatar-text">{{ user.avatar }}</span>
</div>
<div class="profile-info">
<h2>{{ user.name }}</h2>
<p class="email">{{ user.email }}</p>
<div class="profile-meta">
<span class="badge badge-role">{{ user.role }}</span>
<span class="department">
<span class="material-icons">business</span>
{{ user.department }}
</span>
</div>
</div>
<button class="btn-edit">
<span class="material-icons">edit</span>
Edit Profile
</button>
</div>
<div class="profile-details">
<div class="detail-item">
<span class="material-icons">event</span>
<div>
<label>Joined</label>
<span>{{ user.joinDate }}</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons">schedule</span>
<div>
<label>Last Login</label>
<span>{{ user.lastLogin }}</span>
</div>
</div>
</div>
</section>
<!-- Stats Grid -->
<section class="stats-grid">
<div class="stat-card" *ngFor="let stat of stats">
<span class="material-icons stat-icon">{{ stat.icon }}</span>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</section>
<!-- Recent Activity -->
<section class="activity-section">
<h2>
<span class="material-icons">history</span>
Recent Activity
</h2>
<div class="activity-list">
<div class="activity-item" *ngFor="let activity of recentActivity">
<div class="activity-icon">
<span class="material-icons">{{
activity.action.includes('Uploaded') ? 'cloud_upload' :
activity.action.includes('query') ? 'search' :
activity.action.includes('Downloaded') ? 'cloud_download' :
'settings'
}}</span>
</div>
<div class="activity-details">
<div class="activity-action">{{ activity.action }}</div>
<div class="activity-file">{{ activity.file }}</div>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</section>
<!-- Security Section -->
<section class="security-section">
<h2>
<span class="material-icons">security</span>
Security
</h2>
<div class="security-content">
<div class="security-item">
<div class="security-info">
<span class="material-icons">lock</span>
<div>
<h3>Password</h3>
<p>Last changed 3 months ago</p>
</div>
</div>
<button class="btn-secondary">Change Password</button>
</div>
<div class="security-item">
<div class="security-info">
<span class="material-icons">verified_user</span>
<div>
<h3>Two-Factor Authentication</h3>
<p>Add an extra layer of security</p>
</div>
</div>
<button class="btn-secondary">Enable 2FA</button>
</div>
<div class="security-item">
<div class="security-info">
<span class="material-icons">devices</span>
<div>
<h3>Active Sessions</h3>
<p>Manage your active login sessions</p>
</div>
</div>
<button class="btn-secondary">View Sessions</button>
</div>
</div>
</section>
</div>
</div>

View File

@@ -1,36 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-profile',
standalone: true,
imports: [CommonModule],
templateUrl: './profile.html',
styleUrls: ['./profile.css']
})
export class ProfileComponent {
// Hardcoded user data (will be replaced with OAuth integration)
user = {
name: 'John Doe',
email: 'john.doe@warehouse13.com',
avatar: 'JD',
role: 'Administrator',
department: 'Engineering',
joinDate: 'January 15, 2024',
lastLogin: 'October 16, 2025, 2:30 PM'
};
stats = [
{ label: 'Artifacts Uploaded', value: '1,234', icon: 'cloud_upload' },
{ label: 'Queries Run', value: '567', icon: 'search' },
{ label: 'Storage Used', value: '45.2 GB', icon: 'storage' },
{ label: 'Active Since', value: '9 months', icon: 'schedule' }
];
recentActivity = [
{ action: 'Uploaded artifact', file: 'test_results.csv', time: '2 hours ago' },
{ action: 'Ran query', file: 'integration tests', time: '5 hours ago' },
{ action: 'Downloaded artifact', file: 'performance_metrics.json', time: '1 day ago' },
{ action: 'Updated settings', file: 'notification preferences', time: '2 days ago' }
];
}

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

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

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

View File

@@ -1,227 +0,0 @@
.settings-container {
max-width: 900px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.page-header h1 .material-icons {
font-size: 32px;
color: #3b82f6;
}
.subtitle {
color: #94a3b8;
margin: 0;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.settings-section {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-section h2 {
font-size: 18px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.setting-item:last-child {
border-bottom: none;
}
.setting-info {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
}
.setting-info > .material-icons {
font-size: 24px;
color: #64748b;
margin-top: 2px;
}
.setting-info h3 {
font-size: 15px;
font-weight: 500;
color: #f1f5f9;
margin: 0 0 4px 0;
}
.setting-info p {
font-size: 13px;
color: #94a3b8;
margin: 0;
}
.setting-control {
display: flex;
align-items: center;
gap: 12px;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 26px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #3b82f6;
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Theme Buttons */
.theme-btn {
padding: 8px 20px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.theme-btn:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
.theme-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
/* Buttons */
.btn-secondary {
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
text-decoration: none;
display: inline-block;
}
.btn-secondary:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
/* Badge */
.badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
/* Code */
.endpoint {
padding: 6px 12px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #3b82f6;
}
/* Responsive */
@media (max-width: 768px) {
.settings-container {
padding: 16px;
}
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.setting-control {
width: 100%;
justify-content: flex-end;
}
}

View File

@@ -1,167 +0,0 @@
<div class="settings-container">
<header class="page-header">
<h1>
<span class="material-icons">settings</span>
Settings
</h1>
<p class="subtitle">Manage your Warehouse13 preferences</p>
</header>
<div class="settings-content">
<!-- General Settings -->
<section class="settings-section">
<h2>General</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">palette</span>
<div>
<h3>Theme</h3>
<p>Choose your preferred color scheme</p>
</div>
</div>
<div class="setting-control">
<button
[class.active]="settings.theme === 'dark'"
(click)="changeTheme('dark')"
class="theme-btn">
Dark
</button>
<button
[class.active]="settings.theme === 'light'"
(click)="changeTheme('light')"
class="theme-btn">
Light
</button>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">notifications</span>
<div>
<h3>Notifications</h3>
<p>Enable desktop notifications for updates</p>
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input
type="checkbox"
[checked]="settings.notifications"
(change)="toggleNotifications()">
<span class="slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">refresh</span>
<div>
<h3>Auto Refresh</h3>
<p>Automatically refresh artifact list</p>
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input
type="checkbox"
[checked]="settings.autoRefresh"
(change)="toggleAutoRefresh()">
<span class="slider"></span>
</label>
</div>
</div>
</section>
<!-- Storage Settings -->
<section class="settings-section">
<h2>Storage</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">storage</span>
<div>
<h3>Default Storage Backend</h3>
<p>Currently using: MinIO</p>
</div>
</div>
<div class="setting-control">
<span class="badge badge-success">Connected</span>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">cloud</span>
<div>
<h3>Upload Limit</h3>
<p>Maximum file size: 500MB</p>
</div>
</div>
</div>
</section>
<!-- API Settings -->
<section class="settings-section">
<h2>API</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">link</span>
<div>
<h3>API Endpoint</h3>
<p>Backend service endpoint</p>
</div>
</div>
<div class="setting-control">
<code class="endpoint">/api/v1</code>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">key</span>
<div>
<h3>API Keys</h3>
<p>Manage API authentication tokens</p>
</div>
</div>
<div class="setting-control">
<button class="btn-secondary">Manage Keys</button>
</div>
</div>
</section>
<!-- About Section -->
<section class="settings-section">
<h2>About</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">info</span>
<div>
<h3>Version</h3>
<p>Warehouse13 v1.0.0</p>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">description</span>
<div>
<h3>Documentation</h3>
<p>View API documentation and guides</p>
</div>
</div>
<div class="setting-control">
<a href="/docs" target="_blank" class="btn-secondary">
View Docs
</a>
</div>
</div>
</section>
</div>
</div>

View File

@@ -1,30 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule],
templateUrl: './settings.html',
styleUrls: ['./settings.css']
})
export class SettingsComponent {
settings = {
theme: 'dark',
notifications: true,
autoRefresh: false,
refreshInterval: 30
};
toggleNotifications() {
this.settings.notifications = !this.settings.notifications;
}
toggleAutoRefresh() {
this.settings.autoRefresh = !this.settings.autoRefresh;
}
changeTheme(theme: string) {
this.settings.theme = theme;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

@@ -0,0 +1,51 @@
export interface Artifact {
id: number;
filename: string;
file_type: string;
file_size: number;
storage_path: string;
test_name?: string;
test_suite?: string;
test_result?: 'pass' | 'fail' | 'skip' | 'error';
test_config?: any;
custom_metadata?: any;
description?: string;
tags: string[];
version?: string;
created_at: string;
updated_at: string;
event_id?: string;
binaries?: string[];
}
export interface ArtifactQuery {
filename?: string;
file_type?: string;
test_name?: string;
test_suite?: string;
test_result?: string;
tags?: string[];
start_date?: string;
end_date?: string;
limit?: number;
offset?: number;
}
export interface ApiInfo {
deployment_mode: string;
storage_backend: string;
}
export interface UploadResponse {
id: number;
filename: string;
message: string;
}
export interface Tag {
id?: number;
name: string;
scope?: string;
color?: string;
created_at?: string;
}

View 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`);
}
}

View 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 = 1000, 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);
}
}

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

View File

@@ -6,6 +6,7 @@
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>

View File

@@ -1,6 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

@@ -33,21 +33,6 @@ header {
header h1 {
font-size: 28px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.logo {
font-family: 'Courier New', monospace;
font-weight: 700;
font-size: 24px;
color: #60a5fa;
letter-spacing: -1px;
padding: 2px 4px;
border: 2px solid #60a5fa;
border-radius: 4px;
background: rgba(96, 165, 250, 0.1);
}
.header-info {

70
frontend/src/styles.scss Normal file
View File

@@ -0,0 +1,70 @@
/* Global styles for Obsidian - Dark Theme from main branch */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f172a;
min-height: 100vh;
padding: 20px;
color: #e2e8f0;
}
html, body {
height: 100%;
margin: 0;
}
/* 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;
}
/* Main color variables matching main branch */
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #e2e8f0;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--text-dark-muted: #64748b;
--accent-blue: #60a5fa;
--accent-blue-dark: #3b82f6;
--accent-blue-darker: #2563eb;
--gradient-start: #1e3a8a;
--gradient-end: #4338ca;
--success-bg: #064e3b;
--success-text: #6ee7b7;
--error-bg: #7f1d1d;
--error-text: #fca5a5;
--warning-bg: #78350f;
--warning-text: #fcd34d;
--badge-bg: #1e3a8a;
--badge-text: #93c5fd;
}

View File

@@ -0,0 +1,34 @@
[CmdletBinding()]
param(
[Parameter(Position=0)]
[ValidateSet("public", "artifactory")]
[string]$RegistryType = "public"
)
$ErrorActionPreference = "Stop"
switch ($RegistryType) {
"public" {
Write-Host "Switching to public npm registry..." -ForegroundColor Yellow
Copy-Item ".npmrc.public" ".npmrc" -Force
Write-Host "[OK] Now using registry.npmjs.org" -ForegroundColor Green
Write-Host ""
Write-Host "To install dependencies:" -ForegroundColor White
Write-Host " npm ci --force" -ForegroundColor Cyan
}
"artifactory" {
Write-Host "Switching to Artifactory registry..." -ForegroundColor Yellow
Copy-Item ".npmrc.artifactory" ".npmrc" -Force
Write-Host "[OK] Now using Artifactory registry" -ForegroundColor Green
Write-Host ""
Write-Host "Make sure to set environment variables if authentication is required:" -ForegroundColor White
Write-Host ' $env:ARTIFACTORY_AUTH_TOKEN = "your_token"' -ForegroundColor Cyan
Write-Host ""
Write-Host "To install dependencies:" -ForegroundColor White
Write-Host " npm ci --force" -ForegroundColor Cyan
}
}
Write-Host ""
Write-Host "Current .npmrc contents:" -ForegroundColor White
Get-Content ".npmrc"

View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Script to switch between npm registries
# Usage: ./switch-registry.sh [public|artifactory]
set -e
REGISTRY_TYPE=${1:-public}
case $REGISTRY_TYPE in
public)
echo "Switching to public npm registry..."
cp .npmrc.public .npmrc
echo "✓ Now using registry.npmjs.org"
echo ""
echo "To install dependencies:"
echo " npm ci --force"
;;
artifactory)
echo "Switching to Artifactory registry..."
cp .npmrc.artifactory .npmrc
echo "✓ Now using Artifactory registry"
echo ""
echo "Make sure to set environment variables if authentication is required:"
echo " export ARTIFACTORY_AUTH_TOKEN=your_token"
echo ""
echo "To install dependencies:"
echo " npm ci --force"
;;
*)
echo "Usage: $0 [public|artifactory]"
echo ""
echo "Options:"
echo " public - Use registry.npmjs.org (default)"
echo " artifactory - Use Artifactory npm registry"
exit 1
;;
esac
echo ""
echo "Current .npmrc contents:"
cat .npmrc

View File

@@ -6,10 +6,10 @@
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
"files": [
"src/main.ts"
],
"exclude": [
"src/**/*.spec.ts"
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -3,6 +3,7 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
@@ -10,25 +11,17 @@
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
}

View File

@@ -9,6 +9,7 @@
]
},
"include": [
"src/**/*.ts"
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

13
helm/Chart.yaml Normal file
View 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

View File

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

View File

@@ -1,14 +1,14 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "warehouse13.name" -}}
{{- define "datalake.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "warehouse13.fullname" -}}
{{- define "datalake.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "warehouse13.chart" -}}
{{- define "datalake.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "warehouse13.labels" -}}
helm.sh/chart: {{ include "warehouse13.chart" . }}
{{ include "warehouse13.selectorLabels" . }}
{{- define "datalake.labels" -}}
helm.sh/chart: {{ include "datalake.chart" . }}
{{ include "datalake.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
@@ -43,29 +43,18 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
{{/*
Selector labels
*/}}
{{- define "warehouse13.selectorLabels" -}}
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
{{- define "datalake.selectorLabels" -}}
app.kubernetes.io/name: {{ include "datalake.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "warehouse13.serviceAccountName" -}}
{{- define "datalake.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "warehouse13.fullname" .) .Values.serviceAccount.name }}
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
PostgreSQL connection string
*/}}
{{- define "warehouse13.postgresUrl" -}}
{{- if .Values.app.env.databaseUrl }}
{{- .Values.app.env.databaseUrl }}
{{- else }}
{{- printf "postgresql://%s:%s@warehouse13-postgres:%d/%s" .Values.postgres.auth.username .Values.postgres.auth.password (.Values.postgres.service.port | int) .Values.postgres.auth.database }}
{{- end }}
{{- end }}

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

View File

@@ -2,9 +2,9 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: warehouse13-ingress
name: {{ include "datalake.fullname" . }}
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
{{- include "datalake.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
@@ -33,9 +33,9 @@ spec:
pathType: {{ .pathType }}
backend:
service:
name: {{ printf "warehouse13-%s" .backend }}
name: {{ include "datalake.fullname" $ }}
port:
number: {{ $.Values.app.service.port }}
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

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

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

View File

@@ -2,9 +2,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "warehouse13.serviceAccountName" . }}
name: {{ include "datalake.serviceAccountName" . }}
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
{{- include "datalake.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}

111
helm/values.yaml Normal file
View 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"

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More