Compare commits
80 Commits
d7390a3a80
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019208ad0e | ||
|
|
af4fd324eb | ||
|
|
3d30c84497 | ||
|
|
6f8247e4fd | ||
|
|
014dcb5b09 | ||
|
|
2f786aa14f | ||
|
|
2e9d2ca143 | ||
|
|
4f0e7013e4 | ||
|
|
c1df449f57 | ||
|
|
a07c3ccd9c | ||
| 5fe92cde25 | |||
|
|
a4e4cb4c5f | ||
|
|
ec5d9916ba | ||
| 34ce3ef998 | |||
| d70cbdb12f | |||
| b63597345a | |||
| a910b8270a | |||
| 27afda2d70 | |||
|
|
247d207e3b | ||
|
|
a12df3306d | ||
|
|
29eb7358df | ||
|
|
28daa5c078 | ||
|
|
79f17c423b | ||
|
|
50bcd35e68 | ||
|
|
61cd5c471a | ||
|
|
e83ecf4717 | ||
|
|
53b37c2e16 | ||
|
|
a9bf480954 | ||
|
|
4afbc53420 | ||
|
|
d0594fb161 | ||
|
|
7a0e0c95aa | ||
| 943cd6935b | |||
| 4a270dbfe3 | |||
|
|
e08ab62a32 | ||
|
|
c7ae399615 | ||
| 33d06bc94d | |||
| e2e5c683e4 | |||
|
|
838e145598 | ||
| 15e0f886d7 | |||
|
|
7c50f0a59a | ||
|
|
6508363c12 | ||
| 80242b9602 | |||
| 4641cbb3fa | |||
| 59001222a0 | |||
|
|
5e958ac8c3 | ||
| 7126c618ea | |||
|
|
543617cc08 | ||
|
|
a1151d5e89 | ||
|
|
10b95ec5ef | ||
|
|
bf5e5c7542 | ||
|
|
090361cf66 | ||
|
|
18e70cd445 | ||
| a256e01444 | |||
|
|
5920bf1617 | ||
|
|
122e3f2edc | ||
|
|
2584e92af2 | ||
|
|
1016fee300 | ||
|
|
cda0e99ce7 | ||
|
|
a08b7af8ca | ||
|
|
7305cef18c | ||
|
|
450faad45c | ||
|
|
16d1afc44b | ||
|
|
a607a3f15b | ||
|
|
009c3261c8 | ||
|
|
e7532f0324 | ||
| 7ea16fe48e | |||
| c4d325ecd3 | |||
| 9e3af1fc07 | |||
| bd6c2ce9c1 | |||
| 59defcefe5 | |||
| d9c6f490f0 | |||
| a3a2cec9cf | |||
| 2054181228 | |||
| 6c01329f27 | |||
| c177be326c | |||
| 972bb50c64 | |||
| 20a4ea1655 | |||
| 0856ca5b7a | |||
| 0e5abbbece | |||
| d69c209101 |
@@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
# Python cache
|
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
@@ -6,50 +5,15 @@ __pycache__
|
|||||||
.Python
|
.Python
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
# Node modules (critical for fixing esbuild platform issue)
|
|
||||||
node_modules
|
|
||||||
frontend/node_modules
|
|
||||||
*/node_modules
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
frontend/dist
|
|
||||||
frontend/.angular
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
*.env
|
*.env
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Git
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
# Documentation
|
|
||||||
*.md
|
*.md
|
||||||
|
|
||||||
# IDE files
|
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Configuration
|
|
||||||
helm/
|
helm/
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
# Development files
|
|
||||||
frontend/.vscode
|
|
||||||
frontend/src/**/*.spec.ts
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage
|
|
||||||
coverage
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ API_PORT=8000
|
|||||||
MAX_UPLOAD_SIZE=524288000
|
MAX_UPLOAD_SIZE=524288000
|
||||||
|
|
||||||
# NPM Configuration (for frontend build)
|
# NPM Configuration (for frontend build)
|
||||||
# Leave blank or set to https://registry.npmjs.org/ for default npm registry
|
# Default: https://registry.npmjs.org/ (public npm registry)
|
||||||
# Set to your custom npm proxy/registry URL if needed (e.g., http://your-nexus-server:8081/repository/npm-proxy/)
|
# For restricted environments, set to your custom npm proxy/registry URL
|
||||||
NPM_REGISTRY=
|
# Example: http://your-nexus-server:8081/repository/npm-proxy/
|
||||||
|
NPM_REGISTRY=https://registry.npmjs.org/
|
||||||
|
|||||||
26
.gitea/workflows/docker.yml
Normal file
26
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.bitstorm.ca # e.g., docker.io for Docker Hub
|
||||||
|
username: ${{ secrets.REGISTRY_LOGIN }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: git.bitstorm.ca/bitforge/warehouse13:latest
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -86,11 +86,10 @@ helm/charts/
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
*.tmp
|
*.tmp
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# Node/NPM
|
# Node.js
|
||||||
frontend/node_modules/
|
package-lock.json
|
||||||
frontend/.angular/
|
**/package-lock.json
|
||||||
frontend/dist/
|
|
||||||
# Keep .npmrc but ignore sensitive auth info
|
# Built static files (generated during Docker build from Angular)
|
||||||
# The active .npmrc will be generated from .npmrc.public or .npmrc.artifactory
|
static/
|
||||||
|
|||||||
182
.gitlab-ci.yml
182
.gitlab-ci.yml
@@ -1,164 +1,42 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
variables:
|
|
||||||
DOCKER_DRIVER: overlay2
|
|
||||||
DOCKER_TLS_CERTDIR: "/certs"
|
|
||||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
|
||||||
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
|
||||||
|
|
||||||
# Test stage
|
build_container:
|
||||||
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
|
stage: build
|
||||||
image: docker:24
|
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
|
||||||
services:
|
variables:
|
||||||
- docker:24-dind
|
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
|
||||||
before_script:
|
before_script:
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
- mkdir -p /tmp/buildah-storage
|
||||||
|
- export BUILDAH_ROOT="/tmp/buildah-storage"
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
script:
|
script:
|
||||||
- echo "Building Docker image..."
|
# - 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 .
|
||||||
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
|
- buildah pull git.bitstorm.ca/bitforge/warehouse13:latest
|
||||||
- docker push $IMAGE_TAG
|
- buildah tag git.bitstorm.ca/bitforge/warehouse13:latest $IMAGE_NAME
|
||||||
- docker push $LATEST_TAG
|
- echo "Pushing $IMAGE_NAME"
|
||||||
only:
|
- buildah push $IMAGE_NAME
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- tags
|
|
||||||
|
|
||||||
# Deploy to development
|
deploy_helm_charts:
|
||||||
deploy:dev:
|
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: alpine/helm:latest
|
image:
|
||||||
before_script:
|
name: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
|
||||||
- apk add --no-cache curl
|
parallel:
|
||||||
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
matrix:
|
||||||
- chmod +x kubectl
|
# - ENV: "prod"
|
||||||
- mv kubectl /usr/local/bin/
|
# VALUES_FILE: "helm/values-prod.yaml"
|
||||||
- mkdir -p ~/.kube
|
# CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
|
||||||
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
|
# NAMESPACE: "bsf-services-namespace"
|
||||||
|
# ONLY: "main"
|
||||||
|
- ENV: "dev"
|
||||||
|
VALUES_FILE: "helm/warehouse13/values.yaml"
|
||||||
|
CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
|
||||||
|
NAMESPACE: "bsf-services-dev-namespace"
|
||||||
|
# ONLY: ["branches", "!main"]
|
||||||
script:
|
script:
|
||||||
- echo "Deploying to development environment..."
|
- kubectl config use-context $CONTEXT
|
||||||
|
- echo "Deploy - $CI_REGISTRY_NAME - $CI_COMMIT_REF_SLUG - $CI_COMMIT_SHORT_SHA"
|
||||||
- |
|
- |
|
||||||
helm upgrade --install datalake-dev ./helm \
|
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
|
||||||
--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
|
|
||||||
164
.gitlab-ci.yml.old
Normal file
164
.gitlab-ci.yml.old
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DOCKER_DRIVER: overlay2
|
||||||
|
DOCKER_TLS_CERTDIR: "/certs"
|
||||||
|
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||||
|
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
|
||||||
|
|
||||||
|
# Test stage
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: python:3.11-slim
|
||||||
|
before_script:
|
||||||
|
- apt-get update && apt-get install -y gcc postgresql-client
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- pip install pytest pytest-asyncio httpx
|
||||||
|
script:
|
||||||
|
- echo "Running tests..."
|
||||||
|
- python -m pytest tests/ -v || echo "No tests found, skipping"
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
|
||||||
|
# Lint stage
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
image: python:3.11-slim
|
||||||
|
before_script:
|
||||||
|
- pip install flake8 black
|
||||||
|
script:
|
||||||
|
- echo "Running linters..."
|
||||||
|
- flake8 app/ --max-line-length=120 --ignore=E203,W503 || true
|
||||||
|
- black --check app/ || true
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: docker:24
|
||||||
|
services:
|
||||||
|
- docker:24-dind
|
||||||
|
before_script:
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
script:
|
||||||
|
- echo "Building Docker image..."
|
||||||
|
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
|
||||||
|
- docker push $IMAGE_TAG
|
||||||
|
- docker push $LATEST_TAG
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- tags
|
||||||
|
|
||||||
|
# Deploy to development
|
||||||
|
deploy:dev:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine/helm:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||||
|
- chmod +x kubectl
|
||||||
|
- mv kubectl /usr/local/bin/
|
||||||
|
- mkdir -p ~/.kube
|
||||||
|
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
|
||||||
|
script:
|
||||||
|
- echo "Deploying to development environment..."
|
||||||
|
- |
|
||||||
|
helm upgrade --install datalake-dev ./helm \
|
||||||
|
--namespace datalake-dev \
|
||||||
|
--create-namespace \
|
||||||
|
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||||
|
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||||
|
--set ingress.enabled=true \
|
||||||
|
--set ingress.hosts[0].host=datalake-dev.example.com \
|
||||||
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
|
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||||
|
--wait \
|
||||||
|
--timeout 5m
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
|
url: https://datalake-dev.example.com
|
||||||
|
only:
|
||||||
|
- develop
|
||||||
|
when: manual
|
||||||
|
|
||||||
|
# Deploy to staging
|
||||||
|
deploy:staging:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine/helm:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||||
|
- chmod +x kubectl
|
||||||
|
- mv kubectl /usr/local/bin/
|
||||||
|
- mkdir -p ~/.kube
|
||||||
|
- echo "$KUBE_CONFIG_STAGING" | base64 -d > ~/.kube/config
|
||||||
|
script:
|
||||||
|
- echo "Deploying to staging environment..."
|
||||||
|
- |
|
||||||
|
helm upgrade --install datalake-staging ./helm \
|
||||||
|
--namespace datalake-staging \
|
||||||
|
--create-namespace \
|
||||||
|
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||||
|
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||||
|
--set ingress.enabled=true \
|
||||||
|
--set ingress.hosts[0].host=datalake-staging.example.com \
|
||||||
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
|
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||||
|
--set resources.requests.cpu=1000m \
|
||||||
|
--set resources.requests.memory=1Gi \
|
||||||
|
--wait \
|
||||||
|
--timeout 5m
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
url: https://datalake-staging.example.com
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
when: manual
|
||||||
|
|
||||||
|
# Deploy to production
|
||||||
|
deploy:prod:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine/helm:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||||
|
- chmod +x kubectl
|
||||||
|
- mv kubectl /usr/local/bin/
|
||||||
|
- mkdir -p ~/.kube
|
||||||
|
- echo "$KUBE_CONFIG_PROD" | base64 -d > ~/.kube/config
|
||||||
|
script:
|
||||||
|
- echo "Deploying to production environment..."
|
||||||
|
- |
|
||||||
|
helm upgrade --install datalake ./helm \
|
||||||
|
--namespace datalake-prod \
|
||||||
|
--create-namespace \
|
||||||
|
--set image.repository=$CI_REGISTRY_IMAGE \
|
||||||
|
--set image.tag=$CI_COMMIT_SHORT_SHA \
|
||||||
|
--set replicaCount=3 \
|
||||||
|
--set ingress.enabled=true \
|
||||||
|
--set ingress.hosts[0].host=datalake.example.com \
|
||||||
|
--set ingress.hosts[0].paths[0].path=/ \
|
||||||
|
--set ingress.hosts[0].paths[0].pathType=Prefix \
|
||||||
|
--set resources.requests.cpu=2000m \
|
||||||
|
--set resources.requests.memory=2Gi \
|
||||||
|
--set autoscaling.enabled=true \
|
||||||
|
--set autoscaling.minReplicas=3 \
|
||||||
|
--set autoscaling.maxReplicas=10 \
|
||||||
|
--wait \
|
||||||
|
--timeout 10m
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://datalake.example.com
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
when: manual
|
||||||
47
Dockerfile
47
Dockerfile
@@ -1,49 +1,42 @@
|
|||||||
# Multi-stage build: First stage builds Angular frontend
|
# Multi-stage build: First stage builds Angular frontend
|
||||||
FROM node:18-alpine as frontend-builder
|
FROM node:20.11-alpine3.19 AS frontend-build
|
||||||
|
|
||||||
# Build argument to select npm registry (public or artifactory)
|
# Accept npm registry as build argument
|
||||||
ARG NPM_REGISTRY=public
|
ARG NPM_REGISTRY=https://registry.npmjs.org/
|
||||||
ARG ARTIFACTORY_AUTH_TOKEN=""
|
|
||||||
|
|
||||||
# Install dependencies for native modules
|
|
||||||
RUN apk add --no-cache python3 make g++
|
|
||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
# Copy package files and registry configs
|
# Copy package files
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
COPY frontend/.npmrc.${NPM_REGISTRY} ./.npmrc
|
|
||||||
|
|
||||||
# If using artifactory and auth token is provided, configure it
|
# Configure npm registry if custom registry is provided
|
||||||
RUN if [ "$NPM_REGISTRY" = "artifactory" ] && [ -n "$ARTIFACTORY_AUTH_TOKEN" ]; then \
|
RUN if [ "$NPM_REGISTRY" != "https://registry.npmjs.org/" ]; then \
|
||||||
echo "Configuring Artifactory authentication..."; \
|
echo "Using custom npm registry: $NPM_REGISTRY"; \
|
||||||
|
npm config set registry "$NPM_REGISTRY"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean install dependencies
|
# Install dependencies (ignore package-lock.json if using custom registry)
|
||||||
RUN npm ci --force
|
RUN npm install
|
||||||
|
|
||||||
# Copy frontend source
|
# Copy source code
|
||||||
COPY frontend/src ./src
|
COPY frontend/ ./
|
||||||
COPY frontend/public ./public
|
|
||||||
COPY frontend/angular.json ./
|
|
||||||
COPY frontend/tsconfig*.json ./
|
|
||||||
|
|
||||||
# Build the Angular app for production
|
# Build for production
|
||||||
RUN npm run build
|
RUN npm run build:prod
|
||||||
|
|
||||||
# Second stage: Python backend with Angular static files
|
# Second stage: Python backend with Angular frontend
|
||||||
FROM python:3.11-alpine
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies for Alpine
|
# Install system dependencies for Alpine
|
||||||
|
# Alpine uses apk instead of apt-get and is lighter/faster
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
gcc \
|
gcc \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
postgresql-dev \
|
postgresql-dev \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
linux-headers \
|
linux-headers
|
||||||
curl
|
|
||||||
|
|
||||||
# Copy requirements and install Python dependencies
|
# Copy requirements and install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
@@ -55,8 +48,8 @@ COPY utils/ ./utils/
|
|||||||
COPY alembic/ ./alembic/
|
COPY alembic/ ./alembic/
|
||||||
COPY alembic.ini .
|
COPY alembic.ini .
|
||||||
|
|
||||||
# Copy Angular build from frontend-builder stage
|
# Copy built Angular frontend from first stage to static directory
|
||||||
COPY --from=frontend-builder /frontend/dist/frontend/browser ./static
|
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
|
||||||
|
|
||||||
# Create non-root user (Alpine uses adduser instead of useradd)
|
# Create non-root user (Alpine uses adduser instead of useradd)
|
||||||
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
|
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
@@ -67,7 +60,7 @@ EXPOSE 8000
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost:8000/health
|
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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;"]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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;"]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# 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
114
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Obsidian
|
# Warehouse13
|
||||||
|
|
||||||
**Enterprise Test Artifact Storage**
|
**Enterprise Test Artifact Storage**
|
||||||
|
|
||||||
@@ -36,18 +36,32 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### One-Command Setup
|
### Standard Deployment (Internet Access)
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux/macOS:**
|
||||||
```bash
|
```bash
|
||||||
./scripts/quickstart.sh
|
./quickstart.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows (PowerShell):**
|
**Windows (PowerShell):**
|
||||||
```powershell
|
```powershell
|
||||||
.\scripts\quickstart.ps1
|
.\quickstart.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Air-Gapped/Restricted Environment Deployment
|
||||||
|
|
||||||
|
**For environments with restricted npm access:**
|
||||||
|
```bash
|
||||||
|
./quickstart-airgap.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
1. Builds Angular locally (where npm works)
|
||||||
|
2. Packages pre-built files into Docker
|
||||||
|
3. Starts all services
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions.
|
||||||
|
|
||||||
### Manual Setup with Docker Compose
|
### Manual Setup with Docker Compose
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
@@ -200,35 +214,54 @@ MINIO_BUCKET_NAME=test-artifacts
|
|||||||
|
|
||||||
### Kubernetes with Helm
|
### Kubernetes with Helm
|
||||||
|
|
||||||
1. Build and push Docker image:
|
**Quick Start:**
|
||||||
```bash
|
```bash
|
||||||
docker build -t your-registry/datalake:latest .
|
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
||||||
docker push your-registry/datalake:latest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install with Helm:
|
**Production Deployment:**
|
||||||
```bash
|
```bash
|
||||||
helm install datalake ./helm \
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
--set image.repository=your-registry/datalake \
|
--namespace warehouse13 \
|
||||||
--set image.tag=latest \
|
--create-namespace \
|
||||||
--namespace datalake \
|
--values ./helm/warehouse13/values-production.yaml
|
||||||
--create-namespace
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Access the API:
|
**Air-Gapped Deployment:**
|
||||||
```bash
|
```bash
|
||||||
kubectl port-forward -n datalake svc/datalake 8000:8000
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--create-namespace \
|
||||||
|
--values ./helm/warehouse13/values-airgapped.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Access the Application:**
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helm Documentation
|
||||||
|
|
||||||
|
- **Full Helm Guide:** [HELM-DEPLOYMENT.md](./docs/HELM-DEPLOYMENT.md)
|
||||||
|
- **Chart README:** [helm/warehouse13/README.md](./helm/warehouse13/README.md)
|
||||||
|
- **Quick Start:** [helm/warehouse13/QUICKSTART.md](./helm/warehouse13/QUICKSTART.md)
|
||||||
|
- **Example Configurations:**
|
||||||
|
- Development: [values-dev.yaml](./helm/warehouse13/values-dev.yaml)
|
||||||
|
- Production: [values-production.yaml](./helm/warehouse13/values-production.yaml)
|
||||||
|
- Air-Gapped: [values-airgapped.yaml](./helm/warehouse13/values-airgapped.yaml)
|
||||||
|
|
||||||
### Helm Configuration
|
### Helm Configuration
|
||||||
|
|
||||||
Edit `helm/values.yaml` to customize:
|
All component images are fully configurable in `helm/warehouse13/values.yaml`:
|
||||||
- Replica count
|
- PostgreSQL image and version
|
||||||
- Resource limits
|
- MinIO image and version
|
||||||
- Storage backend (S3 vs MinIO)
|
- API image and version
|
||||||
- Ingress settings
|
- Frontend image and version
|
||||||
- PostgreSQL settings
|
- Resource limits and requests
|
||||||
- Autoscaling
|
- Storage backend configuration
|
||||||
|
- Ingress and TLS settings
|
||||||
|
- Persistence and storage classes
|
||||||
|
|
||||||
### GitLab CI/CD
|
### GitLab CI/CD
|
||||||
|
|
||||||
@@ -269,23 +302,6 @@ Store compiled binaries, test data files, or any binary artifacts with full meta
|
|||||||
|
|
||||||
## Development
|
## 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
|
### Running Tests
|
||||||
```bash
|
```bash
|
||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
@@ -320,26 +336,6 @@ alembic upgrade head
|
|||||||
- Verify `MINIO_ENDPOINT` is correct
|
- Verify `MINIO_ENDPOINT` is correct
|
||||||
- Check MinIO credentials
|
- Check MinIO credentials
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Detailed documentation is available in the `docs/` folder:
|
|
||||||
|
|
||||||
- **[Quick Start Guide](docs/QUICKSTART.md)** - Get started in minutes
|
|
||||||
- **[API Documentation](docs/API.md)** - Complete API reference
|
|
||||||
- **[Architecture](docs/ARCHITECTURE.md)** - System design and architecture
|
|
||||||
- **[Features](docs/FEATURES.md)** - Detailed feature descriptions
|
|
||||||
- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment instructions
|
|
||||||
- **[Frontend Setup](docs/FRONTEND_SETUP.md)** - Angular frontend setup
|
|
||||||
- **[Frontend Usage](docs/FRONTEND_USAGE.md)** - Using the web UI
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
Helper scripts are available in the `scripts/` folder:
|
|
||||||
|
|
||||||
- **`quickstart.sh` / `quickstart.ps1`** - Quick start with Docker Compose
|
|
||||||
- **`quickstart-build.sh`** - Quick start with image rebuild
|
|
||||||
- **`dev-start.sh` / `dev-start.ps1`** - Start development environment
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Your License Here]
|
[Your License Here]
|
||||||
|
|||||||
117
app/api/tags.py
117
app/api/tags.py
@@ -1,117 +0,0 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from app.database import get_db
|
|
||||||
from app.models.tag import Tag
|
|
||||||
from app.schemas.tag import TagCreate, TagUpdate, TagResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/tags", tags=["tags"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=TagResponse, status_code=201)
|
|
||||||
async def create_tag(tag: TagCreate, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Create a new tag
|
|
||||||
|
|
||||||
- **name**: Tag name (unique, required)
|
|
||||||
- **description**: Tag description (optional)
|
|
||||||
- **color**: Hex color code (optional, e.g., #FF5733)
|
|
||||||
"""
|
|
||||||
# Check if tag already exists
|
|
||||||
existing_tag = db.query(Tag).filter(Tag.name == tag.name).first()
|
|
||||||
if existing_tag:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Tag with name '{tag.name}' already exists")
|
|
||||||
|
|
||||||
db_tag = Tag(**tag.model_dump())
|
|
||||||
db.add(db_tag)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_tag)
|
|
||||||
|
|
||||||
return db_tag
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[TagResponse])
|
|
||||||
async def list_tags(
|
|
||||||
limit: int = Query(default=100, le=1000),
|
|
||||||
offset: int = Query(default=0, ge=0),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""List all tags with pagination"""
|
|
||||||
tags = db.query(Tag).order_by(Tag.name).offset(offset).limit(limit).all()
|
|
||||||
return tags
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tag_id}", response_model=TagResponse)
|
|
||||||
async def get_tag(tag_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""Get tag by ID"""
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/name/{tag_name}", response_model=TagResponse)
|
|
||||||
async def get_tag_by_name(tag_name: str, db: Session = Depends(get_db)):
|
|
||||||
"""Get tag by name"""
|
|
||||||
tag = db.query(Tag).filter(Tag.name == tag_name).first()
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{tag_id}", response_model=TagResponse)
|
|
||||||
async def update_tag(tag_id: int, tag_update: TagUpdate, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Update a tag
|
|
||||||
|
|
||||||
- **name**: Tag name (optional)
|
|
||||||
- **description**: Tag description (optional)
|
|
||||||
- **color**: Hex color code (optional)
|
|
||||||
"""
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
# Check if new name conflicts with existing tag
|
|
||||||
if tag_update.name and tag_update.name != tag.name:
|
|
||||||
existing_tag = db.query(Tag).filter(Tag.name == tag_update.name).first()
|
|
||||||
if existing_tag:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Tag with name '{tag_update.name}' already exists")
|
|
||||||
|
|
||||||
# Update fields
|
|
||||||
update_data = tag_update.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(tag, field, value)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
db.refresh(tag)
|
|
||||||
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{tag_id}")
|
|
||||||
async def delete_tag(tag_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""Delete a tag"""
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
db.delete(tag)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return {"message": f"Tag '{tag.name}' deleted successfully"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/search", response_model=List[TagResponse])
|
|
||||||
async def search_tags(
|
|
||||||
query: str = Query(..., min_length=1, description="Search query"),
|
|
||||||
limit: int = Query(default=100, le=1000),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Search tags by name or description"""
|
|
||||||
tags = db.query(Tag).filter(
|
|
||||||
(Tag.name.ilike(f"%{query}%")) | (Tag.description.ilike(f"%{query}%"))
|
|
||||||
).order_by(Tag.name).limit(limit).all()
|
|
||||||
|
|
||||||
return tags
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.models.artifact import Base as ArtifactBase
|
from app.models.artifact import Base
|
||||||
from app.models.tag import Base as TagBase
|
|
||||||
|
|
||||||
engine = create_engine(settings.database_url)
|
engine = create_engine(settings.database_url)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
@@ -10,8 +9,7 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Initialize database tables"""
|
"""Initialize database tables"""
|
||||||
ArtifactBase.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
TagBase.metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|||||||
78
app/main.py
78
app/main.py
@@ -1,15 +1,13 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from app.api.artifacts import router as artifacts_router
|
from app.api.artifacts import router as artifacts_router
|
||||||
from app.api.seed import router as seed_router
|
from app.api.seed import router as seed_router
|
||||||
from app.api.tags import router as tags_router
|
|
||||||
from app.database import init_db
|
from app.database import init_db
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -21,8 +19,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Test Artifact Data Lake",
|
title="Warehouse13",
|
||||||
description="API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
|
description="Enterprise Test Artifact Storage - API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc"
|
redoc_url="/redoc"
|
||||||
@@ -40,10 +38,9 @@ app.add_middleware(
|
|||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(artifacts_router)
|
app.include_router(artifacts_router)
|
||||||
app.include_router(seed_router)
|
app.include_router(seed_router)
|
||||||
app.include_router(tags_router)
|
|
||||||
|
|
||||||
# Static files configuration - will be set up after routes
|
# Static directory setup
|
||||||
static_dir = Path("/app/static")
|
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
@@ -60,7 +57,7 @@ async def startup_event():
|
|||||||
async def api_root():
|
async def api_root():
|
||||||
"""API root endpoint"""
|
"""API root endpoint"""
|
||||||
return {
|
return {
|
||||||
"message": "Test Artifact Data Lake API",
|
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"deployment_mode": settings.deployment_mode,
|
"deployment_mode": settings.deployment_mode,
|
||||||
@@ -69,17 +66,41 @@ async def api_root():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def serve_angular_app():
|
async def ui_root():
|
||||||
"""Serve Angular app index.html"""
|
"""Serve the UI"""
|
||||||
static_dir = Path("/app/static")
|
index_path = os.path.join(static_dir, "index.html")
|
||||||
if static_dir.exists():
|
if os.path.exists(index_path):
|
||||||
return FileResponse(static_dir / "index.html")
|
return FileResponse(index_path)
|
||||||
else:
|
else:
|
||||||
# Fallback if static files not found
|
|
||||||
return {
|
return {
|
||||||
"message": "Test Artifact Data Lake API",
|
"message": "Warehouse13 - Enterprise Test Artifact Storage",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": "/docs",
|
"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,
|
"deployment_mode": settings.deployment_mode,
|
||||||
"storage_backend": settings.storage_backend
|
"storage_backend": settings.storage_backend
|
||||||
}
|
}
|
||||||
@@ -91,31 +112,6 @@ async def health_check():
|
|||||||
return {"status": "healthy"}
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ class Artifact(Base):
|
|||||||
test_suite = Column(String(500), index=True)
|
test_suite = Column(String(500), index=True)
|
||||||
test_config = Column(JSON)
|
test_config = Column(JSON)
|
||||||
test_result = Column(String(50), index=True) # pass, fail, skip, error
|
test_result = Column(String(50), index=True) # pass, fail, skip, error
|
||||||
sim_source_id = Column(String(500), index=True) # SIM source identifier for grouping
|
|
||||||
|
# SIM source grouping - allows multiple artifacts per source
|
||||||
|
sim_source_id = Column(String(100), index=True) # Groups artifacts from same SIM source
|
||||||
|
|
||||||
# Additional metadata
|
# Additional metadata
|
||||||
custom_metadata = Column(JSON)
|
custom_metadata = Column(JSON)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
from sqlalchemy import Column, String, Integer, DateTime, Text
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(Base):
|
|
||||||
__tablename__ = "tags"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
name = Column(String(100), unique=True, nullable=False, index=True)
|
|
||||||
description = Column(Text)
|
|
||||||
color = Column(String(7)) # Hex color code, e.g., #FF5733
|
|
||||||
|
|
||||||
# Timestamps
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Tag(id={self.id}, name='{self.name}')>"
|
|
||||||
@@ -8,7 +8,7 @@ class ArtifactCreate(BaseModel):
|
|||||||
test_suite: Optional[str] = None
|
test_suite: Optional[str] = None
|
||||||
test_config: Optional[Dict[str, Any]] = None
|
test_config: Optional[Dict[str, Any]] = None
|
||||||
test_result: Optional[str] = None
|
test_result: Optional[str] = None
|
||||||
sim_source_id: Optional[str] = None
|
sim_source_id: Optional[str] = None # Groups artifacts from same SIM source
|
||||||
custom_metadata: Optional[Dict[str, Any]] = None
|
custom_metadata: Optional[Dict[str, Any]] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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
156
dev-start.ps1
@@ -1,156 +0,0 @@
|
|||||||
[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
89
dev-start.sh
@@ -1,89 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:15
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: user
|
|
||||||
POSTGRES_PASSWORD: password
|
|
||||||
POSTGRES_DB: datalake
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U user"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
minio:
|
|
||||||
image: minio/minio:latest
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: minioadmin
|
|
||||||
MINIO_ROOT_PASSWORD: minioadmin
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
api:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
environment:
|
|
||||||
DATABASE_URL: postgresql://user:password@postgres:5432/datalake
|
|
||||||
STORAGE_BACKEND: minio
|
|
||||||
MINIO_ENDPOINT: minio:9000
|
|
||||||
MINIO_ACCESS_KEY: minioadmin
|
|
||||||
MINIO_SECRET_KEY: minioadmin
|
|
||||||
MINIO_BUCKET_NAME: test-artifacts
|
|
||||||
MINIO_SECURE: "false"
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
minio:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.frontend
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:80"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
minio_data:
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
name: warehouse13
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
@@ -35,7 +37,11 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
container_name: warehouse13-app
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
@@ -52,11 +58,10 @@ services:
|
|||||||
minio:
|
minio:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 3
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
@@ -24,25 +24,47 @@ This uses `Dockerfile.frontend` which:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments)
|
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments) ⭐ RECOMMENDED
|
||||||
|
|
||||||
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access or when esbuild platform binaries cannot be downloaded.
|
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access.
|
||||||
|
|
||||||
**Requirements:**
|
**Requirements:**
|
||||||
- Node.js 24+ installed locally
|
- Node.js 18+ installed locally (on a machine with npm access)
|
||||||
- npm installed locally
|
- npm installed locally
|
||||||
- No internet required during Docker build
|
- 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:**
|
**Usage:**
|
||||||
|
|
||||||
### Step 1: Build Angular app locally
|
### Quick Start (Recommended)
|
||||||
```bash
|
```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
|
cd frontend
|
||||||
npm install # Only needed once or when dependencies change
|
npm install # Only needed once or when dependencies change
|
||||||
npm run build:prod
|
npm run build:prod
|
||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This creates `frontend/dist/frontend/browser/` which Docker will copy.
|
||||||
|
|
||||||
### Step 2: Update docker-compose.yml
|
### Step 2: Update docker-compose.yml
|
||||||
Edit `docker-compose.yml` and change the frontend dockerfile:
|
Edit `docker-compose.yml` and change the frontend dockerfile:
|
||||||
|
|
||||||
@@ -71,26 +93,16 @@ This uses `Dockerfile.frontend.prebuilt` which:
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### esbuild Platform Binary Issues
|
### Build Tool Package Issues
|
||||||
|
|
||||||
If you see errors like:
|
If you see errors about missing packages like:
|
||||||
```
|
```
|
||||||
Could not resolve "@esbuild/darwin-arm64"
|
Cannot find package "vite"
|
||||||
|
Cannot find package "esbuild"
|
||||||
|
Cannot find package "rollup"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Solution 1:** Use Option 2 (Pre-built) above
|
**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 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
|
### Custom NPM Registry
|
||||||
|
|
||||||
@@ -109,5 +121,20 @@ NPM_REGISTRY=http://your-proxy ./quickstart.sh
|
|||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
- **Development/Cloud**: Use Option 1 (standard)
|
- **Development/Cloud**: Use Option 1 (standard)
|
||||||
- **Air-gapped/Enterprise**: Use Option 2 (pre-built)
|
- **Air-gapped/Enterprise**: Use Option 2 (pre-built) ⭐ **RECOMMENDED**
|
||||||
- **CI/CD**: Use Option 2 for faster, more reliable builds
|
- **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.
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
# 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
|
|
||||||
517
docs/HELM-DEPLOYMENT.md
Normal file
517
docs/HELM-DEPLOYMENT.md
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# Warehouse13 - Kubernetes Deployment with Helm
|
||||||
|
|
||||||
|
This guide covers deploying Warehouse13 to Kubernetes using the official Helm chart.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Prerequisites](#prerequisites)
|
||||||
|
2. [Quick Start](#quick-start)
|
||||||
|
3. [Deployment Scenarios](#deployment-scenarios)
|
||||||
|
4. [Configuration](#configuration)
|
||||||
|
5. [Post-Deployment](#post-deployment)
|
||||||
|
6. [Upgrading](#upgrading)
|
||||||
|
7. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes 1.19+ cluster
|
||||||
|
- Helm 3.0+
|
||||||
|
- kubectl configured to access your cluster
|
||||||
|
- Persistent volume provisioner (for production deployments)
|
||||||
|
|
||||||
|
### Installing Helm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
brew install helm
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
choco install kubernetes-helm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Standard Deployment (Internet Access)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create namespace
|
||||||
|
kubectl create namespace warehouse13
|
||||||
|
|
||||||
|
# Install with default values
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13
|
||||||
|
|
||||||
|
# Wait for pods to be ready
|
||||||
|
kubectl wait --for=condition=ready pod \
|
||||||
|
--all --namespace warehouse13 --timeout=300s
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Access the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||||
|
|
||||||
|
# API
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||||
|
|
||||||
|
# MinIO Console
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||||
|
```
|
||||||
|
|
||||||
|
Then visit:
|
||||||
|
- Frontend: http://localhost:4200
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
- MinIO Console: http://localhost:9001
|
||||||
|
|
||||||
|
## Deployment Scenarios
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
For local testing or CI/CD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install warehouse13-dev ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13-dev \
|
||||||
|
--create-namespace \
|
||||||
|
--values ./helm/warehouse13/values-dev.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Single replica for all services
|
||||||
|
- emptyDir storage (no persistence)
|
||||||
|
- Minimal resource requests
|
||||||
|
- Always pull latest dev images
|
||||||
|
|
||||||
|
### Production Environment
|
||||||
|
|
||||||
|
For production with ingress and high availability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First, update the values file with your domain and secrets
|
||||||
|
cp ./helm/warehouse13/values-production.yaml ./my-production-values.yaml
|
||||||
|
|
||||||
|
# Edit the file:
|
||||||
|
# - Set postgres.auth.password
|
||||||
|
# - Set minio.auth.rootUser and rootPassword
|
||||||
|
# - Set ingress.hosts[0].host to your domain
|
||||||
|
# - Update storageClass for your environment
|
||||||
|
|
||||||
|
# Install
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--create-namespace \
|
||||||
|
--values ./my-production-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 3 replicas for API and frontend
|
||||||
|
- Persistent storage with PVCs
|
||||||
|
- Ingress with TLS support
|
||||||
|
- Resource limits and requests
|
||||||
|
- Health checks enabled
|
||||||
|
- Pod anti-affinity for distribution
|
||||||
|
|
||||||
|
### Air-Gapped Environment
|
||||||
|
|
||||||
|
For restricted/disconnected environments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. First, push images to your internal registry
|
||||||
|
# Example using harbor.internal.example.com
|
||||||
|
|
||||||
|
# Pull images (on internet-connected machine)
|
||||||
|
docker pull postgres:15-alpine
|
||||||
|
docker pull minio/minio:latest
|
||||||
|
docker pull warehouse13/api:v1.0.0
|
||||||
|
docker pull warehouse13/frontend:v1.0.0
|
||||||
|
|
||||||
|
# Tag for internal registry
|
||||||
|
docker tag postgres:15-alpine harbor.internal.example.com/library/postgres:15-alpine
|
||||||
|
docker tag minio/minio:latest harbor.internal.example.com/library/minio:latest
|
||||||
|
docker tag warehouse13/api:v1.0.0 harbor.internal.example.com/warehouse13/api:v1.0.0
|
||||||
|
docker tag warehouse13/frontend:v1.0.0 harbor.internal.example.com/warehouse13/frontend:v1.0.0
|
||||||
|
|
||||||
|
# Push to internal registry
|
||||||
|
docker push harbor.internal.example.com/library/postgres:15-alpine
|
||||||
|
docker push harbor.internal.example.com/library/minio:latest
|
||||||
|
docker push harbor.internal.example.com/warehouse13/api:v1.0.0
|
||||||
|
docker push harbor.internal.example.com/warehouse13/frontend:v1.0.0
|
||||||
|
|
||||||
|
# 2. Update the values file with your registry
|
||||||
|
cp ./helm/warehouse13/values-airgapped.yaml ./my-airgapped-values.yaml
|
||||||
|
|
||||||
|
# Edit to match your environment:
|
||||||
|
# - Update all image.repository values
|
||||||
|
# - Set secure passwords
|
||||||
|
# - Configure storage classes
|
||||||
|
# - Add node selectors/tolerations if needed
|
||||||
|
|
||||||
|
# 3. Install on air-gapped cluster
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--create-namespace \
|
||||||
|
--values ./my-airgapped-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- All images from custom registry
|
||||||
|
- Local storage class support
|
||||||
|
- Node selectors for specific nodes
|
||||||
|
- Tolerations for tainted nodes
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Configurable Images
|
||||||
|
|
||||||
|
All component images can be customized:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# PostgreSQL
|
||||||
|
postgres:
|
||||||
|
image:
|
||||||
|
repository: postgres # or your-registry/postgres
|
||||||
|
tag: 15-alpine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# MinIO
|
||||||
|
minio:
|
||||||
|
image:
|
||||||
|
repository: minio/minio # or your-registry/minio
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# API Backend
|
||||||
|
api:
|
||||||
|
image:
|
||||||
|
repository: warehouse13/api # or your-registry/warehouse13-api
|
||||||
|
tag: v1.0.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
frontend:
|
||||||
|
image:
|
||||||
|
repository: warehouse13/frontend # or your-registry/warehouse13-frontend
|
||||||
|
tag: v1.0.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Image Override
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Override images from command line
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--set postgres.image.repository=myregistry.com/postgres \
|
||||||
|
--set postgres.image.tag=15-alpine \
|
||||||
|
--set minio.image.repository=myregistry.com/minio \
|
||||||
|
--set minio.image.tag=latest \
|
||||||
|
--set api.image.repository=myregistry.com/warehouse13-api \
|
||||||
|
--set api.image.tag=v1.0.0 \
|
||||||
|
--set frontend.image.repository=myregistry.com/warehouse13-frontend \
|
||||||
|
--set frontend.image.tag=v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# PostgreSQL storage
|
||||||
|
postgres:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 50Gi
|
||||||
|
storageClass: "fast-ssd" # or "" for default
|
||||||
|
|
||||||
|
# MinIO storage
|
||||||
|
minio:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 500Gi
|
||||||
|
storageClass: "bulk-storage" # or "" for default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# API resources
|
||||||
|
api:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
|
||||||
|
# Frontend resources
|
||||||
|
frontend:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ingress Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "nginx"
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
hosts:
|
||||||
|
- host: warehouse13.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend: frontend
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
backend: api
|
||||||
|
tls:
|
||||||
|
- secretName: warehouse13-tls
|
||||||
|
hosts:
|
||||||
|
- warehouse13.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Deployment
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all pods are running
|
||||||
|
kubectl get pods -n warehouse13
|
||||||
|
|
||||||
|
# Check services
|
||||||
|
kubectl get svc -n warehouse13
|
||||||
|
|
||||||
|
# Check PVCs
|
||||||
|
kubectl get pvc -n warehouse13
|
||||||
|
|
||||||
|
# Check ingress (if enabled)
|
||||||
|
kubectl get ingress -n warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API logs
|
||||||
|
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=100 -f
|
||||||
|
|
||||||
|
# Frontend logs
|
||||||
|
kubectl logs -n warehouse13 -l app.kubernetes.io/component=frontend --tail=100 -f
|
||||||
|
|
||||||
|
# PostgreSQL logs
|
||||||
|
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100 -f
|
||||||
|
|
||||||
|
# MinIO logs
|
||||||
|
kubectl logs -n warehouse13 warehouse13-minio-0 --tail=100 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialize MinIO Bucket
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Port-forward to MinIO console
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||||
|
|
||||||
|
# Open http://localhost:9001
|
||||||
|
# Login with credentials from values.yaml
|
||||||
|
# Create bucket: "artifacts"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
### Upgrade to New Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update image tags in values file
|
||||||
|
# Then run upgrade
|
||||||
|
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--values ./my-production-values.yaml \
|
||||||
|
--wait \
|
||||||
|
--timeout 10m
|
||||||
|
|
||||||
|
# Check rollout status
|
||||||
|
kubectl rollout status deployment/warehouse13-api -n warehouse13
|
||||||
|
kubectl rollout status deployment/warehouse13-frontend -n warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View revision history
|
||||||
|
helm history warehouse13 -n warehouse13
|
||||||
|
|
||||||
|
# Rollback to previous version
|
||||||
|
helm rollback warehouse13 -n warehouse13
|
||||||
|
|
||||||
|
# Rollback to specific revision
|
||||||
|
helm rollback warehouse13 2 -n warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Values Only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update configuration without changing images
|
||||||
|
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--values ./my-updated-values.yaml \
|
||||||
|
--reuse-values
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup and Restore
|
||||||
|
|
||||||
|
### PostgreSQL Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
kubectl exec -n warehouse13 warehouse13-postgres-0 -- \
|
||||||
|
pg_dump -U warehouse13user warehouse13 > backup-$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
cat backup-20241016.sql | kubectl exec -i -n warehouse13 warehouse13-postgres-0 -- \
|
||||||
|
psql -U warehouse13user warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MinIO Client
|
||||||
|
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||||
|
chmod +x mc
|
||||||
|
|
||||||
|
# Configure
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9000:9000
|
||||||
|
mc alias set w13 http://localhost:9000 <access-key> <secret-key>
|
||||||
|
|
||||||
|
# Backup bucket
|
||||||
|
mc mirror w13/artifacts ./backup/artifacts-$(date +%Y%m%d)
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
mc mirror ./backup/artifacts-20241016 w13/artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup all PVCs
|
||||||
|
for pvc in $(kubectl get pvc -n warehouse13 -o name); do
|
||||||
|
pvc_name=$(basename $pvc)
|
||||||
|
kubectl get -n warehouse13 $pvc -o yaml > backup-${pvc_name}.yaml
|
||||||
|
done
|
||||||
|
|
||||||
|
# Backup Helm values
|
||||||
|
helm get values warehouse13 -n warehouse13 > backup-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pods Not Starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check pod status
|
||||||
|
kubectl get pods -n warehouse13
|
||||||
|
|
||||||
|
# Describe pod for events
|
||||||
|
kubectl describe pod <pod-name> -n warehouse13
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
kubectl logs <pod-name> -n warehouse13
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - ImagePullBackOff: Check image repository and credentials
|
||||||
|
# - Pending: Check PVC status and node resources
|
||||||
|
# - CrashLoopBackOff: Check application logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### PVC Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PVC status
|
||||||
|
kubectl get pvc -n warehouse13
|
||||||
|
|
||||||
|
# Describe PVC
|
||||||
|
kubectl describe pvc <pvc-name> -n warehouse13
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Pending: No storage class or insufficient storage
|
||||||
|
# - Bound: PVC is healthy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test PostgreSQL connection
|
||||||
|
kubectl exec -it -n warehouse13 warehouse13-postgres-0 -- \
|
||||||
|
psql -U warehouse13user -d warehouse13
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100
|
||||||
|
|
||||||
|
# Verify secret
|
||||||
|
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ingress Not Working
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check ingress status
|
||||||
|
kubectl get ingress -n warehouse13
|
||||||
|
kubectl describe ingress -n warehouse13 warehouse13-ingress
|
||||||
|
|
||||||
|
# Check ingress controller logs
|
||||||
|
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller
|
||||||
|
|
||||||
|
# Verify TLS certificate
|
||||||
|
kubectl get certificate -n warehouse13
|
||||||
|
kubectl describe certificate -n warehouse13 warehouse13-tls
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check resource usage
|
||||||
|
kubectl top pods -n warehouse13
|
||||||
|
kubectl top nodes
|
||||||
|
|
||||||
|
# Check if pods are being throttled
|
||||||
|
kubectl describe pod <pod-name> -n warehouse13 | grep -A 5 "State:"
|
||||||
|
|
||||||
|
# Increase resources
|
||||||
|
helm upgrade warehouse13 ./helm/warehouse13 \
|
||||||
|
--set api.resources.limits.memory=2Gi \
|
||||||
|
--set api.resources.limits.cpu=2000m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstalling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uninstall the release
|
||||||
|
helm uninstall warehouse13 -n warehouse13
|
||||||
|
|
||||||
|
# Delete PVCs (data will be lost!)
|
||||||
|
kubectl delete pvc -n warehouse13 -l app.kubernetes.io/instance=warehouse13
|
||||||
|
|
||||||
|
# Delete namespace
|
||||||
|
kubectl delete namespace warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Helm Chart README](./helm/warehouse13/README.md)
|
||||||
|
- [Values Documentation](./helm/warehouse13/values.yaml)
|
||||||
|
- [Docker Deployment Guide](./DEPLOYMENT.md)
|
||||||
|
- [Main README](./README.md)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||||
|
- Helm Chart Issues: Tag with `helm` label
|
||||||
220
docs/NPM-PACKAGE-AGE-POLICY.md
Normal file
220
docs/NPM-PACKAGE-AGE-POLICY.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# NPM Package Age Policy
|
||||||
|
|
||||||
|
## Requirement
|
||||||
|
|
||||||
|
All npm packages must be **at least 2 weeks old** before they can be used in this project. This ensures:
|
||||||
|
- Package stability
|
||||||
|
- Security vulnerability disclosure time
|
||||||
|
- Compliance with organizational security policies
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. Package Version Pinning
|
||||||
|
|
||||||
|
The project uses exact version pinning in `package.json` to prevent automatic updates:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/core": "19.2.7", // Exact version, not "^19.2.7" or "~19.2.7"
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `frontend/.npmrc` file enforces this:
|
||||||
|
```
|
||||||
|
save-exact=true
|
||||||
|
package-lock=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Automated Age Checking
|
||||||
|
|
||||||
|
Use the provided script to verify all packages meet the 2-week requirement:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if all packages are at least 2 weeks old
|
||||||
|
node scripts/check-package-age.js
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
- Queries npm registry for publish dates
|
||||||
|
- Calculates age of each package
|
||||||
|
- Fails if any package is newer than 14 days
|
||||||
|
- Shows detailed age information for all packages
|
||||||
|
|
||||||
|
### 3. Installation Process
|
||||||
|
|
||||||
|
**Always use `npm ci` instead of `npm install`:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci # Installs exact versions from package-lock.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why `npm ci`?**
|
||||||
|
- Uses exact versions from `package-lock.json`
|
||||||
|
- Doesn't update `package-lock.json`
|
||||||
|
- Ensures reproducible builds
|
||||||
|
- Faster than `npm install`
|
||||||
|
|
||||||
|
## Updating Packages
|
||||||
|
|
||||||
|
When you need to add or update packages:
|
||||||
|
|
||||||
|
### Step 1: Add Package to package.json
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find a version that's at least 2 weeks old
|
||||||
|
npm view <package-name> time
|
||||||
|
|
||||||
|
# Add exact version to package.json
|
||||||
|
"dependencies": {
|
||||||
|
"new-package": "1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Verify Age
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/check-package-age.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Update Lock File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install --package-lock-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Install and Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Add the age check to your CI/CD pipeline:
|
||||||
|
|
||||||
|
### GitLab CI Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
verify_package_age:
|
||||||
|
stage: validate
|
||||||
|
image: node:18-alpine
|
||||||
|
script:
|
||||||
|
- node scripts/check-package-age.js
|
||||||
|
only:
|
||||||
|
- merge_requests
|
||||||
|
- main
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Check Package Age
|
||||||
|
run: node scripts/check-package-age.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Package is too new" Error
|
||||||
|
|
||||||
|
If a package fails the age check:
|
||||||
|
|
||||||
|
1. **Find an older version:**
|
||||||
|
```bash
|
||||||
|
npm view <package-name> versions --json
|
||||||
|
npm view <package-name>@<older-version> time
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update package.json with older version**
|
||||||
|
|
||||||
|
3. **Re-run age check:**
|
||||||
|
```bash
|
||||||
|
node scripts/check-package-age.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Find Old Enough Version
|
||||||
|
|
||||||
|
If no version meets the 2-week requirement:
|
||||||
|
- Wait until the package is at least 2 weeks old
|
||||||
|
- Look for alternative packages
|
||||||
|
- Request an exception through your security team
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Check current package ages
|
||||||
|
node scripts/check-package-age.js
|
||||||
|
|
||||||
|
# 2. If all pass, install dependencies
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
# 3. Build application
|
||||||
|
npm run build:prod
|
||||||
|
|
||||||
|
# 4. For air-gapped deployment
|
||||||
|
../scripts/build-for-airgap.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts Reference
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `scripts/check-package-age.js` | Verify all packages are ≥ 2 weeks old |
|
||||||
|
| `scripts/pin-old-versions.sh` | Helper script to validate and pin versions |
|
||||||
|
| `scripts/build-for-airgap.sh` | Build frontend for air-gapped deployment |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always commit `package-lock.json`**
|
||||||
|
- Ensures everyone uses the same versions
|
||||||
|
- Required for reproducible builds
|
||||||
|
|
||||||
|
2. **Use `npm ci` in CI/CD**
|
||||||
|
- Faster than `npm install`
|
||||||
|
- Enforces lock file versions
|
||||||
|
- Prevents surprises
|
||||||
|
|
||||||
|
3. **Regular audits**
|
||||||
|
```bash
|
||||||
|
# Check for security vulnerabilities
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
# Check package ages
|
||||||
|
node scripts/check-package-age.js
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Version ranges to avoid**
|
||||||
|
- ❌ `^1.2.3` (allows minor/patch updates)
|
||||||
|
- ❌ `~1.2.3` (allows patch updates)
|
||||||
|
- ❌ `*` or `latest` (allows any version)
|
||||||
|
- ✅ `1.2.3` (exact version only)
|
||||||
|
|
||||||
|
## Package Age Check Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Checking package ages (must be at least 2 weeks old)...
|
||||||
|
|
||||||
|
✓ @angular/common@19.2.7 - 45 days old
|
||||||
|
✓ @angular/compiler@19.2.7 - 45 days old
|
||||||
|
✓ rxjs@7.8.0 - 180 days old
|
||||||
|
❌ new-package@1.0.0 - 5 days old (published 2025-01-12)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
❌ FAILED: 1 package(s) are newer than 2 weeks:
|
||||||
|
|
||||||
|
- new-package@1.0.0 (5 days old, published 2025-01-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or exceptions:
|
||||||
|
- Review with security team
|
||||||
|
- Document in project README
|
||||||
|
- Update this policy as needed
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# Quick Start Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Test Artifact Data Lake platform provides several ways to run the application depending on your use case:
|
|
||||||
|
|
||||||
- **Development**: Backend services in Docker + Frontend dev server with hot reload
|
|
||||||
- **Production**: Complete stack in Docker containers
|
|
||||||
- **Testing**: Various rebuild and cleanup options
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- **Docker Desktop** (Windows/macOS) or **Docker** + **Docker Compose** (Linux)
|
|
||||||
- **Node.js 18+** (for development mode)
|
|
||||||
- **npm** (for development mode)
|
|
||||||
|
|
||||||
## Quick Start Options
|
|
||||||
|
|
||||||
### 1. Development Mode (Recommended for Development)
|
|
||||||
|
|
||||||
**Linux/macOS:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh # Backend services only
|
|
||||||
./dev-start.sh # Backend + Frontend dev server
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```powershell
|
|
||||||
.\quickstart.ps1 # Backend services only
|
|
||||||
.\quickstart.ps1 -FullStack # Complete stack
|
|
||||||
.\quickstart.ps1 -Rebuild # Rebuild containers
|
|
||||||
.\dev-start.ps1 # Backend + Frontend dev server
|
|
||||||
```
|
|
||||||
|
|
||||||
**URLs:**
|
|
||||||
- Frontend: http://localhost:4200 (with hot reload)
|
|
||||||
- API: http://localhost:8000
|
|
||||||
- API Docs: http://localhost:8000/docs
|
|
||||||
- MinIO Console: http://localhost:9001
|
|
||||||
|
|
||||||
### 2. Production Mode (Complete Stack)
|
|
||||||
|
|
||||||
**Linux/macOS:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh --full-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```cmd
|
|
||||||
.\quickstart.ps1 -FullStack
|
|
||||||
```
|
|
||||||
|
|
||||||
**URLs:**
|
|
||||||
- Frontend: http://localhost:80 (production build)
|
|
||||||
- API: http://localhost:8000
|
|
||||||
- MinIO Console: http://localhost:9001
|
|
||||||
|
|
||||||
### 3. Force Rebuild (When Code Changes)
|
|
||||||
|
|
||||||
**Linux/macOS:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh --rebuild # Rebuild backend only
|
|
||||||
./quickstart.sh --rebuild --full-stack # Rebuild complete stack
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```cmd
|
|
||||||
.\quickstart.ps1 -Rebuild # Rebuild backend only
|
|
||||||
.\quickstart.ps1 -Rebuild -FullStack # Rebuild complete stack
|
|
||||||
```
|
|
||||||
|
|
||||||
## Detailed Usage
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
1. **Start backend services:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Start frontend in development mode:**
|
|
||||||
```bash
|
|
||||||
./dev-start.sh
|
|
||||||
```
|
|
||||||
Or manually:
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Make changes to your code** - Frontend will auto-reload
|
|
||||||
|
|
||||||
4. **When backend code changes:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh --rebuild
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Testing
|
|
||||||
|
|
||||||
1. **Build and run complete stack:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh --full-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Test at http://localhost:80**
|
|
||||||
|
|
||||||
3. **When code changes:**
|
|
||||||
```bash
|
|
||||||
./quickstart.sh --rebuild --full-stack
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command Reference
|
|
||||||
|
|
||||||
### quickstart.sh / quickstart.ps1
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| (none) | Start backend services only (default) |
|
|
||||||
| `--full-stack` | Start complete stack including frontend |
|
|
||||||
| `--rebuild` | Force rebuild of containers |
|
|
||||||
| `--help` | Show help message |
|
|
||||||
|
|
||||||
### dev-start.sh / dev-start.ps1
|
|
||||||
|
|
||||||
Starts backend services + frontend development server with hot reload.
|
|
||||||
|
|
||||||
## Stopping Services
|
|
||||||
|
|
||||||
**Backend only:**
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
**Complete stack:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.production.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
**Backend services:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
**Complete stack:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.production.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
**Specific service:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f api
|
|
||||||
docker-compose logs -f postgres
|
|
||||||
docker-compose logs -f minio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and modify as needed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
The quickstart scripts will automatically create this file if it doesn't exist.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container Issues
|
|
||||||
- **Force rebuild:** Use `--rebuild` flag
|
|
||||||
- **Clean everything:** `docker-compose down --volumes --rmi all`
|
|
||||||
- **Check Docker:** Ensure Docker Desktop is running
|
|
||||||
|
|
||||||
### Frontend Issues
|
|
||||||
- **Dependencies:** Run `npm install` in `frontend/` directory
|
|
||||||
- **Port conflicts:** Check if port 4200 is available
|
|
||||||
- **Node version:** Ensure Node.js 18+ is installed
|
|
||||||
|
|
||||||
### Backend Issues
|
|
||||||
- **API not responding:** Wait longer for services to start (can take 30+ seconds)
|
|
||||||
- **Database issues:** Check `docker-compose logs postgres`
|
|
||||||
- **Storage issues:** Check `docker-compose logs minio`
|
|
||||||
|
|
||||||
## Development vs Production
|
|
||||||
|
|
||||||
| Feature | Development | Production |
|
|
||||||
|---------|-------------|------------|
|
|
||||||
| Frontend | Hot reload dev server | Built Angular app |
|
|
||||||
| Port | 4200 | 80 |
|
|
||||||
| Build time | Fast startup | Slower (builds Angular) |
|
|
||||||
| Use case | Development, testing | Demo, staging |
|
|
||||||
|
|
||||||
Choose the mode that best fits your workflow!
|
|
||||||
@@ -164,7 +164,7 @@ curl -X POST "http://localhost:8000/api/v1/artifacts/query" \
|
|||||||
make deploy
|
make deploy
|
||||||
|
|
||||||
# Or directly with Helm
|
# Or directly with Helm
|
||||||
helm install datalake ./helm --namespace datalake --create-namespace
|
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature Flags Usage
|
## Feature Flags Usage
|
||||||
@@ -190,9 +190,8 @@ AWS_REGION=us-east-1
|
|||||||
S3_BUCKET_NAME=your-bucket
|
S3_BUCKET_NAME=your-bucket
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
helm install datalake ./helm \
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
--set config.deploymentMode=cloud \
|
--set global.deploymentMode=cloud
|
||||||
--set aws.enabled=true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's Next
|
## What's Next
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -36,6 +36,7 @@ yarn-error.log
|
|||||||
/libpeerconnection.log
|
/libpeerconnection.log
|
||||||
testem.log
|
testem.log
|
||||||
/typings
|
/typings
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
registry=https://registry.npmjs.org/
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
registry=https://registry.npmjs.org/
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,151 +1,178 @@
|
|||||||
# Test Artifact Data Lake - Angular Frontend
|
# Warehouse13 Frontend - Angular Application
|
||||||
|
|
||||||
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.
|
Modern Angular application for the Warehouse13 Test Artifact Data Lake.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
✅ **Multi-component Architecture**: Built with reusable Angular components
|
- **Angular 20** with standalone components
|
||||||
✅ **Tab Navigation**: Clean tab-based interface for Artifacts, Upload, and Query
|
- **TypeScript** for type safety
|
||||||
✅ **Event ID Support**: Group multiple artifacts under the same event ID
|
- **Reactive Forms** for upload and query functionality
|
||||||
✅ **Expandable Binaries Display**: Show first 4 binaries, expandable for more
|
- **RxJS** for reactive programming
|
||||||
✅ **Advanced Tag Management**: Create tags on-the-spot with database persistence
|
- **Auto-refresh** artifacts every 5 seconds
|
||||||
✅ **Scoped Tags**: Organize tags by scope (project, environment, priority, etc.)
|
- **Client-side sorting and filtering**
|
||||||
✅ **Comprehensive Filtering**: Filter artifacts by all table criteria
|
- **Dark theme** UI
|
||||||
✅ **Real-time Search**: As-you-type filtering in query form
|
- **Responsive design**
|
||||||
✅ **Responsive Design**: Mobile-friendly interface
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
- **TabNavigationComponent**: Manages tab switching between Artifacts, Upload, and Query
|
|
||||||
- **ArtifactsTableComponent**: Displays artifacts with expandable binaries/tags and Event ID support
|
|
||||||
- **UploadFormComponent**: File upload with Event ID and binaries support
|
|
||||||
- **QueryFormComponent**: Advanced search with real-time filtering
|
|
||||||
- **TagManagerComponent**: On-the-spot tag creation with scoped tags
|
|
||||||
|
|
||||||
### Services
|
|
||||||
- **ArtifactService**: Handles all artifact-related API calls
|
|
||||||
- **ApiService**: Manages general API information
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js (v18 or later)
|
|
||||||
- Angular CLI 19
|
|
||||||
|
|
||||||
### Setup
|
- Node.js 24.x or higher
|
||||||
|
- npm 11.x or higher
|
||||||
|
- Backend API running on port 8000
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development Server
|
### Run Development Server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
# or
|
|
||||||
ng serve
|
|
||||||
```
|
```
|
||||||
The app will be available at `http://localhost:4200`
|
|
||||||
|
The application will be available at `http://localhost:4200/`
|
||||||
|
|
||||||
|
The development server includes a proxy configuration that forwards `/api` requests to `http://localhost:8000`.
|
||||||
|
|
||||||
### Build for Production
|
### Build for Production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build:prod
|
||||||
# or
|
|
||||||
ng build
|
|
||||||
```
|
```
|
||||||
Built files will be in `dist/frontend/`
|
|
||||||
|
Build artifacts will be in the `dist/frontend/browser` directory.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── artifacts-list/ # Main artifacts table with sorting, filtering, auto-refresh
|
||||||
|
│ │ ├── upload-form/ # Reactive form for uploading artifacts
|
||||||
|
│ │ └── query-form/ # Advanced query interface
|
||||||
|
│ ├── models/
|
||||||
|
│ │ └── artifact.model.ts # TypeScript interfaces for type safety
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── artifact.ts # HTTP service for API calls
|
||||||
|
│ ├── app.ts # Main app component with routing
|
||||||
|
│ ├── app.config.ts # Application configuration
|
||||||
|
│ └── app.routes.ts # Route definitions
|
||||||
|
├── styles.css # Global dark theme styles
|
||||||
|
└── main.ts # Application bootstrap
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
### ArtifactsListComponent
|
||||||
|
- Displays artifacts in a sortable, filterable table
|
||||||
|
- Auto-refreshes every 5 seconds (toggleable)
|
||||||
|
- Client-side search across all fields
|
||||||
|
- Download and delete actions
|
||||||
|
- Detail modal for full artifact information
|
||||||
|
- Tags displayed as inline badges
|
||||||
|
- SIM source grouping support
|
||||||
|
|
||||||
|
### UploadFormComponent
|
||||||
|
- Reactive form with validation
|
||||||
|
- File upload with drag-and-drop support
|
||||||
|
- Required fields: File, Sim Source, Uploaded By, Tags
|
||||||
|
- Optional fields: SIM Source ID (for grouping), Test Result, Version, Description, Test Config, Custom Metadata
|
||||||
|
- JSON validation for config fields
|
||||||
|
- Real-time upload status feedback
|
||||||
|
|
||||||
|
### QueryFormComponent
|
||||||
|
- Advanced search with multiple filter criteria
|
||||||
|
- Filter by: filename, file type, test name, test suite, test result, SIM source ID, tags, date range
|
||||||
|
- Results emitted to artifacts list
|
||||||
|
|
||||||
## API Integration
|
## API Integration
|
||||||
|
|
||||||
The frontend expects the backend API to be available at:
|
The frontend communicates with the FastAPI backend through the `ArtifactService`:
|
||||||
- Development: Same origin as the frontend
|
|
||||||
- Production: Configurable via environment files
|
|
||||||
|
|
||||||
### Required API Endpoints
|
- `GET /api/v1/artifacts/` - List all artifacts
|
||||||
- `GET /api` - API information
|
- `GET /api/v1/artifacts/:id` - Get single artifact
|
||||||
- `GET /api/v1/artifacts/` - List artifacts
|
- `POST /api/v1/artifacts/upload` - Upload new artifact
|
||||||
- `GET /api/v1/artifacts/{id}` - Get artifact details
|
- `POST /api/v1/artifacts/query` - Query with filters
|
||||||
- `POST /api/v1/artifacts/upload` - Upload artifact
|
- `GET /api/v1/artifacts/:id/download` - Download artifact file
|
||||||
- `DELETE /api/v1/artifacts/{id}` - Delete artifact
|
- `DELETE /api/v1/artifacts/:id` - Delete artifact
|
||||||
- `GET /api/v1/artifacts/{id}/download` - Download artifact
|
- `POST /api/v1/seed/generate/:count` - Generate seed data
|
||||||
- `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
|
|
||||||
|
|
||||||
## Key Features Implementation
|
## Configuration
|
||||||
|
|
||||||
### Event ID Support
|
### Proxy Configuration (`proxy.conf.json`)
|
||||||
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.
|
|
||||||
|
|
||||||
### Expandable Binaries
|
```json
|
||||||
When an artifact has more than 4 associated binaries, only the first 4 are shown with a "+X more" button to expand and see all binaries.
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:8000",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Advanced Tag Management
|
This proxies all `/api` requests to the backend during development.
|
||||||
- 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
|
|
||||||
|
|
||||||
### Comprehensive Filtering
|
## Styling
|
||||||
The query form provides real-time filtering by:
|
|
||||||
- Filename (partial match)
|
|
||||||
- File type
|
|
||||||
- Test name
|
|
||||||
- Test suite
|
|
||||||
- Test result
|
|
||||||
- Tags (comma-separated)
|
|
||||||
- Date range
|
|
||||||
|
|
||||||
Filters are applied immediately as you type, and active filters are displayed as visual chips.
|
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
|
||||||
|
|
||||||
## Architecture Improvements
|
## Browser Support
|
||||||
|
|
||||||
### From Static to Angular
|
- Chrome/Edge (latest)
|
||||||
The original static JavaScript implementation has been converted to:
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
|
||||||
1. **Component-based Architecture**: Each major feature is now a reusable component
|
## Development Tips
|
||||||
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
|
|
||||||
|
|
||||||
### Benefits
|
1. **Hot Reload**: Changes to TypeScript files automatically trigger recompilation
|
||||||
- **Maintainability**: Clear separation of concerns
|
2. **Type Safety**: Use TypeScript interfaces in `models/` for all API responses
|
||||||
- **Reusability**: Components can be reused and extended
|
3. **State Management**: Currently using component-level state; consider NgRx for complex state
|
||||||
- **Testing**: Angular's testing framework support
|
4. **Testing**: Run `npm test` for unit tests (Jasmine/Karma)
|
||||||
- **Performance**: Optimized change detection and lazy loading
|
|
||||||
- **Developer Experience**: Hot reload, TypeScript, and Angular DevTools
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Development
|
For production deployment, build the application and serve the `dist/frontend/browser` directory with your web server (nginx, Apache, etc.).
|
||||||
The Angular frontend can be served during development using `ng serve` and will proxy API calls to the backend.
|
|
||||||
|
|
||||||
### Production
|
Example nginx configuration:
|
||||||
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.
|
|
||||||
|
|
||||||
### Integration with Existing Backend
|
```nginx
|
||||||
The Angular frontend is designed to be a drop-in replacement for the static frontend. Simply:
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
root /path/to/dist/frontend/browser;
|
||||||
|
|
||||||
1. Build the Angular app: `npm run build`
|
location / {
|
||||||
2. Copy contents of `dist/frontend/` to your static files directory
|
try_files $uri $uri/ /index.html;
|
||||||
3. Update your backend to serve the new `index.html`
|
}
|
||||||
4. Ensure API endpoints match the expected interface
|
|
||||||
|
|
||||||
## Browser Support
|
location /api {
|
||||||
- Chrome (latest)
|
proxy_pass http://backend:8000;
|
||||||
- Firefox (latest)
|
}
|
||||||
- Safari (latest)
|
}
|
||||||
- Edge (latest)
|
```
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
- Drag and drop file upload
|
|
||||||
- Bulk operations
|
- [ ] Add NgRx for state management
|
||||||
- Advanced data visualization
|
- [ ] Implement WebSocket for real-time updates
|
||||||
- Real-time updates via WebSocket
|
- [ ] Add Angular Material components
|
||||||
- Export functionality
|
- [ ] Unit and E2E tests
|
||||||
- User authentication integration
|
- [ ] PWA support
|
||||||
|
- [ ] Drag-and-drop file upload
|
||||||
|
- [ ] Bulk operations
|
||||||
|
- [ ] Export to CSV/JSON
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as parent project
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
# 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"
|
|
||||||
```
|
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"cli": {
|
|
||||||
"packageManager": "npm",
|
|
||||||
"analytics": false
|
|
||||||
},
|
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {},
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:application",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/frontend",
|
"outputPath": "dist/frontend",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
@@ -28,7 +20,6 @@
|
|||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -36,13 +27,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
"src/styles.css"
|
||||||
"src/styles.scss"
|
]
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
"optimization" : {
|
||||||
|
"fonts": false
|
||||||
|
},
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
@@ -66,10 +58,7 @@
|
|||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"options": {
|
|
||||||
"proxyConfig": "proxy.conf.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "frontend:build:production"
|
"buildTarget": "frontend:build:production"
|
||||||
@@ -81,17 +70,16 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
"builder": "@angular/build:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular/build:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
"zone.js/testing"
|
"zone.js/testing"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
@@ -99,13 +87,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
"src/styles.css"
|
||||||
"src/styles.scss"
|
]
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
15273
frontend/package-lock.json
generated
15273
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,39 +3,81 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
|
"build:prod": "ng build --configuration production",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test"
|
||||||
"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');\""
|
"prettier": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^19.2.19",
|
"@angular/common": "19.2.x",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/compiler": "19.2.x",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/core": "19.2.x",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/forms": "19.2.x",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/platform-browser": "19.2.x",
|
||||||
"@angular/material": "^19.2.19",
|
"@angular/router": "19.2.x",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
|
||||||
"@angular/router": "^19.2.0",
|
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.2.17",
|
"@angular/build": "19.2.x",
|
||||||
"@angular/cli": "^19.2.17",
|
"@angular/cli": "19.2.x",
|
||||||
"@angular/compiler-cli": "^19.2.0",
|
"@angular/compiler-cli": "19.2.x",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.6.0",
|
"jasmine-core": "~5.9.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.7.2"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
"/api": {
|
"/api": {
|
||||||
"target": "http://localhost:8000",
|
"target": "http://localhost:8000",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true
|
||||||
"logLevel": "debug",
|
|
||||||
"pathRewrite": {
|
|
||||||
"^/api": "/api"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/* App Layout */
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 280px;
|
||||||
|
transition: margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content.sidebar-collapsed {
|
||||||
|
margin-right: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Header */
|
||||||
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 32px;
|
||||||
|
background: #1e293b;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header h1 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
margin-right: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-content {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-header h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth Scrolling */
|
||||||
|
.content-area::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Material Icons Sizing */
|
||||||
|
.material-icons.md-16 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons.md-18 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons.md-20 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-icons.md-24 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
import { ArtifactsListComponent } from './components/artifacts-list/artifacts-list';
|
||||||
|
import { UploadFormComponent } from './components/upload-form/upload-form';
|
||||||
|
import { QueryFormComponent } from './components/query-form/query-form';
|
||||||
|
import { SettingsComponent } from './components/settings/settings';
|
||||||
|
import { ProfileComponent } from './components/profile/profile';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{ path: '', redirectTo: '/artifacts', pathMatch: 'full' },
|
||||||
|
{ path: 'artifacts', component: ArtifactsListComponent },
|
||||||
|
{ path: 'upload', component: UploadFormComponent },
|
||||||
|
{ path: 'query', component: QueryFormComponent },
|
||||||
|
{ path: 'settings', component: SettingsComponent },
|
||||||
|
{ path: 'profile', component: ProfileComponent }
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
|
||||||
import { ArtifactService } from './services/artifact';
|
import { ArtifactService } from './services/artifact';
|
||||||
|
import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
imports: [CommonModule, RouterOutlet, NavSidebarComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="container">
|
<div class="app-layout">
|
||||||
<header>
|
<app-nav-sidebar (sidebarToggled)="onSidebarToggle($event)"></app-nav-sidebar>
|
||||||
<h1>◆ Obsidian</h1>
|
|
||||||
|
<main class="main-content" [class.sidebar-collapsed]="isSidebarCollapsed">
|
||||||
|
<header class="top-header">
|
||||||
|
<h1><span class="logo">[W13]</span> Warehouse13</h1>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span class="badge">{{ deploymentMode }}</span>
|
<span class="badge">{{ deploymentMode }}</span>
|
||||||
<span class="badge">{{ storageBackend }}</span>
|
<span class="badge">{{ storageBackend }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="tabs">
|
<div class="content-area">
|
||||||
<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>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
styleUrls: ['./app.css']
|
styleUrls: ['./app.css']
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
deploymentMode: string = '';
|
deploymentMode: string = '';
|
||||||
storageBackend: string = '';
|
storageBackend: string = '';
|
||||||
|
isSidebarCollapsed: boolean = false;
|
||||||
|
|
||||||
constructor(private artifactService: ArtifactService) {}
|
constructor(private artifactService: ArtifactService) {}
|
||||||
|
|
||||||
@@ -50,4 +45,8 @@ export class AppComponent implements OnInit {
|
|||||||
error: (err) => console.error('Failed to load API info:', err)
|
error: (err) => console.error('Failed to load API info:', err)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSidebarToggle(isCollapsed: boolean) {
|
||||||
|
this.isSidebarCollapsed = isCollapsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
276
frontend/src/app/components/nav-sidebar/nav-sidebar.css
Normal file
276
frontend/src/app/components/nav-sidebar/nav-sidebar.css
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: 280px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Button */
|
||||||
|
.toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: -18px;
|
||||||
|
top: 20px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3b82f6;
|
||||||
|
border: 2px solid #0f172a;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.5);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 1001;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn .material-icons {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Section */
|
||||||
|
.user-section {
|
||||||
|
padding: 24px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .user-section {
|
||||||
|
padding: 24px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .user-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #3b82f6;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Items */
|
||||||
|
.nav-items {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .nav-item {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: #3b82f6;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .nav-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar:not(.collapsed) {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 60px;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
left: auto;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label,
|
||||||
|
.user-info {
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
40
frontend/src/app/components/nav-sidebar/nav-sidebar.html
Normal file
40
frontend/src/app/components/nav-sidebar/nav-sidebar.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<aside class="sidebar" [class.collapsed]="isCollapsed">
|
||||||
|
<!-- Toggle Button -->
|
||||||
|
<button class="toggle-btn" (click)="toggleSidebar()" aria-label="Toggle sidebar">
|
||||||
|
<span class="material-icons">{{ isCollapsed ? 'chevron_right' : 'chevron_left' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- User Profile Section -->
|
||||||
|
<div class="user-section">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<span class="avatar-text">{{ user.avatar }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-info" *ngIf="!isCollapsed">
|
||||||
|
<div class="user-name">{{ user.name }}</div>
|
||||||
|
<div class="user-email">{{ user.email }}</div>
|
||||||
|
<div class="user-role">{{ user.role }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Items -->
|
||||||
|
<nav class="nav-items">
|
||||||
|
<a
|
||||||
|
*ngFor="let item of navItems"
|
||||||
|
[routerLink]="item.route"
|
||||||
|
routerLinkActive="active"
|
||||||
|
class="nav-item"
|
||||||
|
[attr.aria-label]="item.label"
|
||||||
|
>
|
||||||
|
<span class="material-icons nav-icon">{{ item.icon }}</span>
|
||||||
|
<span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- App Info Footer -->
|
||||||
|
<div class="sidebar-footer" *ngIf="!isCollapsed">
|
||||||
|
<div class="app-info">
|
||||||
|
<div class="app-name">Warehouse13</div>
|
||||||
|
<div class="app-version">v1.0.0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
43
frontend/src/app/components/nav-sidebar/nav-sidebar.ts
Normal file
43
frontend/src/app/components/nav-sidebar/nav-sidebar.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Component, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
route: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-nav-sidebar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterLink, RouterLinkActive],
|
||||||
|
templateUrl: './nav-sidebar.html',
|
||||||
|
styleUrls: ['./nav-sidebar.css']
|
||||||
|
})
|
||||||
|
export class NavSidebarComponent {
|
||||||
|
isCollapsed = false;
|
||||||
|
|
||||||
|
@Output() sidebarToggled = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
// Hardcoded user data for now (will be replaced with OAuth)
|
||||||
|
user = {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john.doe@warehouse13.com',
|
||||||
|
avatar: 'JD',
|
||||||
|
role: 'Administrator'
|
||||||
|
};
|
||||||
|
|
||||||
|
navItems: NavItem[] = [
|
||||||
|
{ route: '/artifacts', label: 'Artifacts', icon: 'inventory_2' },
|
||||||
|
{ route: '/upload', label: 'Upload', icon: 'cloud_upload' },
|
||||||
|
{ route: '/query', label: 'Query', icon: 'search' },
|
||||||
|
{ route: '/profile', label: 'Profile', icon: 'person' },
|
||||||
|
{ route: '/settings', label: 'Settings', icon: 'settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
this.isCollapsed = !this.isCollapsed;
|
||||||
|
this.sidebarToggled.emit(this.isCollapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
395
frontend/src/app/components/profile/profile.css
Normal file
395
frontend/src/app/components/profile/profile.css
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
.profile-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 .material-icons {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Card */
|
||||||
|
.profile-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar-large {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4px 24px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-role {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department .material-icons {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
background: transparent;
|
||||||
|
color: #3b82f6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item > .material-icons {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item span {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Section */
|
||||||
|
.activity-section,
|
||||||
|
.security-section {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section h2,
|
||||||
|
.security-section h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section h2 .material-icons,
|
||||||
|
.security-section h2 .material-icons {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon .material-icons {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-action {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-file {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Security Section */
|
||||||
|
.security-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-info > .material-icons {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-info h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-info p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: transparent;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar-large {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
128
frontend/src/app/components/profile/profile.html
Normal file
128
frontend/src/app/components/profile/profile.html
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<div class="profile-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>
|
||||||
|
<span class="material-icons">person</span>
|
||||||
|
Profile
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle">Manage your account information</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="profile-content">
|
||||||
|
<!-- User Info Card -->
|
||||||
|
<section class="profile-card">
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="profile-avatar-large">
|
||||||
|
<span class="avatar-text">{{ user.avatar }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-info">
|
||||||
|
<h2>{{ user.name }}</h2>
|
||||||
|
<p class="email">{{ user.email }}</p>
|
||||||
|
<div class="profile-meta">
|
||||||
|
<span class="badge badge-role">{{ user.role }}</span>
|
||||||
|
<span class="department">
|
||||||
|
<span class="material-icons">business</span>
|
||||||
|
{{ user.department }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-edit">
|
||||||
|
<span class="material-icons">edit</span>
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="material-icons">event</span>
|
||||||
|
<div>
|
||||||
|
<label>Joined</label>
|
||||||
|
<span>{{ user.joinDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="material-icons">schedule</span>
|
||||||
|
<div>
|
||||||
|
<label>Last Login</label>
|
||||||
|
<span>{{ user.lastLogin }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<section class="stats-grid">
|
||||||
|
<div class="stat-card" *ngFor="let stat of stats">
|
||||||
|
<span class="material-icons stat-icon">{{ stat.icon }}</span>
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-value">{{ stat.value }}</div>
|
||||||
|
<div class="stat-label">{{ stat.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<section class="activity-section">
|
||||||
|
<h2>
|
||||||
|
<span class="material-icons">history</span>
|
||||||
|
Recent Activity
|
||||||
|
</h2>
|
||||||
|
<div class="activity-list">
|
||||||
|
<div class="activity-item" *ngFor="let activity of recentActivity">
|
||||||
|
<div class="activity-icon">
|
||||||
|
<span class="material-icons">{{
|
||||||
|
activity.action.includes('Uploaded') ? 'cloud_upload' :
|
||||||
|
activity.action.includes('query') ? 'search' :
|
||||||
|
activity.action.includes('Downloaded') ? 'cloud_download' :
|
||||||
|
'settings'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-details">
|
||||||
|
<div class="activity-action">{{ activity.action }}</div>
|
||||||
|
<div class="activity-file">{{ activity.file }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-time">{{ activity.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Security Section -->
|
||||||
|
<section class="security-section">
|
||||||
|
<h2>
|
||||||
|
<span class="material-icons">security</span>
|
||||||
|
Security
|
||||||
|
</h2>
|
||||||
|
<div class="security-content">
|
||||||
|
<div class="security-item">
|
||||||
|
<div class="security-info">
|
||||||
|
<span class="material-icons">lock</span>
|
||||||
|
<div>
|
||||||
|
<h3>Password</h3>
|
||||||
|
<p>Last changed 3 months ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary">Change Password</button>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<div class="security-info">
|
||||||
|
<span class="material-icons">verified_user</span>
|
||||||
|
<div>
|
||||||
|
<h3>Two-Factor Authentication</h3>
|
||||||
|
<p>Add an extra layer of security</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary">Enable 2FA</button>
|
||||||
|
</div>
|
||||||
|
<div class="security-item">
|
||||||
|
<div class="security-info">
|
||||||
|
<span class="material-icons">devices</span>
|
||||||
|
<div>
|
||||||
|
<h3>Active Sessions</h3>
|
||||||
|
<p>Manage your active login sessions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-secondary">View Sessions</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
36
frontend/src/app/components/profile/profile.ts
Normal file
36
frontend/src/app/components/profile/profile.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './profile.html',
|
||||||
|
styleUrls: ['./profile.css']
|
||||||
|
})
|
||||||
|
export class ProfileComponent {
|
||||||
|
// Hardcoded user data (will be replaced with OAuth integration)
|
||||||
|
user = {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john.doe@warehouse13.com',
|
||||||
|
avatar: 'JD',
|
||||||
|
role: 'Administrator',
|
||||||
|
department: 'Engineering',
|
||||||
|
joinDate: 'January 15, 2024',
|
||||||
|
lastLogin: 'October 16, 2025, 2:30 PM'
|
||||||
|
};
|
||||||
|
|
||||||
|
stats = [
|
||||||
|
{ label: 'Artifacts Uploaded', value: '1,234', icon: 'cloud_upload' },
|
||||||
|
{ label: 'Queries Run', value: '567', icon: 'search' },
|
||||||
|
{ label: 'Storage Used', value: '45.2 GB', icon: 'storage' },
|
||||||
|
{ label: 'Active Since', value: '9 months', icon: 'schedule' }
|
||||||
|
];
|
||||||
|
|
||||||
|
recentActivity = [
|
||||||
|
{ action: 'Uploaded artifact', file: 'test_results.csv', time: '2 hours ago' },
|
||||||
|
{ action: 'Ran query', file: 'integration tests', time: '5 hours ago' },
|
||||||
|
{ action: 'Downloaded artifact', file: 'performance_metrics.json', time: '1 day ago' },
|
||||||
|
{ action: 'Updated settings', file: 'notification preferences', time: '2 days ago' }
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
227
frontend/src/app/components/settings/settings.css
Normal file
227
frontend/src/app/components/settings/settings.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
.settings-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 .material-icons {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info > .material-icons {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-info p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 48px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #334155;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Buttons */
|
||||||
|
.theme-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: transparent;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: transparent;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
.badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code */
|
||||||
|
.endpoint {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.settings-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-control {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
167
frontend/src/app/components/settings/settings.html
Normal file
167
frontend/src/app/components/settings/settings.html
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<div class="settings-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>
|
||||||
|
<span class="material-icons">settings</span>
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle">Manage your Warehouse13 preferences</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<!-- General Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>General</h2>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">palette</span>
|
||||||
|
<div>
|
||||||
|
<h3>Theme</h3>
|
||||||
|
<p>Choose your preferred color scheme</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<button
|
||||||
|
[class.active]="settings.theme === 'dark'"
|
||||||
|
(click)="changeTheme('dark')"
|
||||||
|
class="theme-btn">
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
[class.active]="settings.theme === 'light'"
|
||||||
|
(click)="changeTheme('light')"
|
||||||
|
class="theme-btn">
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">notifications</span>
|
||||||
|
<div>
|
||||||
|
<h3>Notifications</h3>
|
||||||
|
<p>Enable desktop notifications for updates</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[checked]="settings.notifications"
|
||||||
|
(change)="toggleNotifications()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">refresh</span>
|
||||||
|
<div>
|
||||||
|
<h3>Auto Refresh</h3>
|
||||||
|
<p>Automatically refresh artifact list</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[checked]="settings.autoRefresh"
|
||||||
|
(change)="toggleAutoRefresh()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Storage Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Storage</h2>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">storage</span>
|
||||||
|
<div>
|
||||||
|
<h3>Default Storage Backend</h3>
|
||||||
|
<p>Currently using: MinIO</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<span class="badge badge-success">Connected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">cloud</span>
|
||||||
|
<div>
|
||||||
|
<h3>Upload Limit</h3>
|
||||||
|
<p>Maximum file size: 500MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- API Settings -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>API</h2>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">link</span>
|
||||||
|
<div>
|
||||||
|
<h3>API Endpoint</h3>
|
||||||
|
<p>Backend service endpoint</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<code class="endpoint">/api/v1</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">key</span>
|
||||||
|
<div>
|
||||||
|
<h3>API Keys</h3>
|
||||||
|
<p>Manage API authentication tokens</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<button class="btn-secondary">Manage Keys</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- About Section -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>About</h2>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">info</span>
|
||||||
|
<div>
|
||||||
|
<h3>Version</h3>
|
||||||
|
<p>Warehouse13 v1.0.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<span class="material-icons">description</span>
|
||||||
|
<div>
|
||||||
|
<h3>Documentation</h3>
|
||||||
|
<p>View API documentation and guides</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<a href="/docs" target="_blank" class="btn-secondary">
|
||||||
|
View Docs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
30
frontend/src/app/components/settings/settings.ts
Normal file
30
frontend/src/app/components/settings/settings.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-settings',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './settings.html',
|
||||||
|
styleUrls: ['./settings.css']
|
||||||
|
})
|
||||||
|
export class SettingsComponent {
|
||||||
|
settings = {
|
||||||
|
theme: 'dark',
|
||||||
|
notifications: true,
|
||||||
|
autoRefresh: false,
|
||||||
|
refreshInterval: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleNotifications() {
|
||||||
|
this.settings.notifications = !this.settings.notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAutoRefresh() {
|
||||||
|
this.settings.autoRefresh = !this.settings.autoRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTheme(theme: string) {
|
||||||
|
this.settings.theme = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
.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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { Component, Output, EventEmitter } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatChipsModule } from '@angular/material/chips';
|
|
||||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { ArtifactService } from '../../services/artifact.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-upload-form',
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatSelectModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatChipsModule,
|
|
||||||
MatProgressBarModule,
|
|
||||||
MatSnackBarModule
|
|
||||||
],
|
|
||||||
templateUrl: './upload-form.component.html',
|
|
||||||
styleUrl: './upload-form.component.scss'
|
|
||||||
})
|
|
||||||
export class UploadFormComponent {
|
|
||||||
@Output() uploadSuccess = new EventEmitter<void>();
|
|
||||||
|
|
||||||
selectedFile: File | null = null;
|
|
||||||
uploading = false;
|
|
||||||
uploadStatus = '';
|
|
||||||
uploadStatusType: 'success' | 'error' | '' = '';
|
|
||||||
|
|
||||||
formData = {
|
|
||||||
testName: '',
|
|
||||||
testSuite: '',
|
|
||||||
testResult: '',
|
|
||||||
version: '',
|
|
||||||
description: '',
|
|
||||||
tags: '',
|
|
||||||
testConfig: '',
|
|
||||||
customMetadata: '',
|
|
||||||
eventId: '',
|
|
||||||
binaries: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
testResults = ['pass', 'fail', 'skip', 'error'];
|
|
||||||
|
|
||||||
constructor(private artifactService: ArtifactService) {}
|
|
||||||
|
|
||||||
onFileSelected(event: any): void {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
this.selectedFile = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetForm(): void {
|
|
||||||
this.selectedFile = null;
|
|
||||||
this.formData = {
|
|
||||||
testName: '',
|
|
||||||
testSuite: '',
|
|
||||||
testResult: '',
|
|
||||||
version: '',
|
|
||||||
description: '',
|
|
||||||
tags: '',
|
|
||||||
testConfig: '',
|
|
||||||
customMetadata: '',
|
|
||||||
eventId: '',
|
|
||||||
binaries: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileInput = document.getElementById('file') as HTMLInputElement;
|
|
||||||
if (fileInput) {
|
|
||||||
fileInput.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showUploadStatus(message: string, success: boolean): void {
|
|
||||||
this.uploadStatus = message;
|
|
||||||
this.uploadStatusType = success ? 'success' : 'error';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.uploadStatus = '';
|
|
||||||
this.uploadStatusType = '';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadArtifact(): void {
|
|
||||||
if (!this.selectedFile) {
|
|
||||||
this.showUploadStatus('Please select a file to upload', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uploading = true;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', this.selectedFile);
|
|
||||||
|
|
||||||
const fields = ['testName', 'testSuite', 'testResult', 'version', 'description', 'eventId'];
|
|
||||||
fields.forEach(field => {
|
|
||||||
const key = field === 'testName' ? 'test_name' :
|
|
||||||
field === 'testSuite' ? 'test_suite' :
|
|
||||||
field === 'testResult' ? 'test_result' :
|
|
||||||
field === 'eventId' ? 'event_id' : field;
|
|
||||||
|
|
||||||
const value = this.formData[field as keyof typeof this.formData];
|
|
||||||
if (value) {
|
|
||||||
formData.append(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.formData.tags) {
|
|
||||||
const tagsArray = this.formData.tags.split(',').map(t => t.trim()).filter(t => t);
|
|
||||||
formData.append('tags', JSON.stringify(tagsArray));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.formData.binaries) {
|
|
||||||
const binariesArray = this.formData.binaries.split(',').map(b => b.trim()).filter(b => b);
|
|
||||||
formData.append('binaries', JSON.stringify(binariesArray));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.formData.testConfig) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.formData.testConfig);
|
|
||||||
formData.append('test_config', this.formData.testConfig);
|
|
||||||
} catch (e) {
|
|
||||||
this.showUploadStatus('Invalid Test Config JSON', false);
|
|
||||||
this.uploading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.formData.customMetadata) {
|
|
||||||
try {
|
|
||||||
JSON.parse(this.formData.customMetadata);
|
|
||||||
formData.append('custom_metadata', this.formData.customMetadata);
|
|
||||||
} catch (e) {
|
|
||||||
this.showUploadStatus('Invalid Custom Metadata JSON', false);
|
|
||||||
this.uploading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.artifactService.uploadArtifact(formData).subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
this.showUploadStatus(`Successfully uploaded: ${response.filename}`, true);
|
|
||||||
this.resetForm();
|
|
||||||
this.uploadSuccess.emit();
|
|
||||||
this.uploading = false;
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
this.showUploadStatus('Upload failed: ' + (error.error?.detail || error.message), false);
|
|
||||||
this.uploading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getResultIcon(result: string): string {
|
|
||||||
switch (result) {
|
|
||||||
case 'pass': return 'check_circle';
|
|
||||||
case 'fail': return 'cancel';
|
|
||||||
case 'skip': return 'skip_next';
|
|
||||||
case 'error': return 'error';
|
|
||||||
default: return 'help';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class NotificationService {
|
|
||||||
|
|
||||||
constructor(private snackBar: MatSnackBar) {}
|
|
||||||
|
|
||||||
showSuccess(message: string, duration: number = 3000): void {
|
|
||||||
this.snackBar.open(message, 'Close', {
|
|
||||||
duration,
|
|
||||||
panelClass: ['success-snackbar'],
|
|
||||||
horizontalPosition: 'center',
|
|
||||||
verticalPosition: 'bottom'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message: string, duration: number = 5000): void {
|
|
||||||
this.snackBar.open(message, 'Close', {
|
|
||||||
duration,
|
|
||||||
panelClass: ['error-snackbar'],
|
|
||||||
horizontalPosition: 'center',
|
|
||||||
verticalPosition: 'bottom'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showInfo(message: string, duration: number = 3000): void {
|
|
||||||
this.snackBar.open(message, 'Close', {
|
|
||||||
duration,
|
|
||||||
panelClass: ['info-snackbar'],
|
|
||||||
horizontalPosition: 'center',
|
|
||||||
verticalPosition: 'bottom'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showWarning(message: string, duration: number = 4000): void {
|
|
||||||
this.snackBar.open(message, 'Close', {
|
|
||||||
duration,
|
|
||||||
panelClass: ['warning-snackbar'],
|
|
||||||
horizontalPosition: 'center',
|
|
||||||
verticalPosition: 'bottom'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showConfirmation(message: string, action: string = 'Confirm'): Promise<boolean> {
|
|
||||||
const snackBarRef = this.snackBar.open(message, action, {
|
|
||||||
duration: 10000,
|
|
||||||
panelClass: ['confirmation-snackbar'],
|
|
||||||
horizontalPosition: 'center',
|
|
||||||
verticalPosition: 'bottom'
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
snackBarRef.onAction().subscribe(() => resolve(true));
|
|
||||||
snackBarRef.afterDismissed().subscribe((info) => {
|
|
||||||
if (!info.dismissedByAction) {
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { AppComponent } from './app/app.component';
|
import { AppComponent } from './app/app';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig)
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|||||||
@@ -33,6 +33,21 @@ header {
|
|||||||
header h1 {
|
header h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #60a5fa;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border: 2px solid #60a5fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(96, 165, 250, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-info {
|
.header-info {
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
[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"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
"outDir": "./out-tsc/app",
|
"outDir": "./out-tsc/app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"files": [
|
|
||||||
"src/main.ts"
|
|
||||||
],
|
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.d.ts"
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist/out-tsc",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
@@ -11,17 +10,25 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022"
|
"module": "preserve"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
"strictInjectionParameters": true,
|
"strictInjectionParameters": true,
|
||||||
"strictInputAccessModifiers": true,
|
"strictInputAccessModifiers": true,
|
||||||
|
"typeCheckHostBindings": true,
|
||||||
"strictTemplates": true
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.spec.ts",
|
"src/**/*.ts"
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: datalake
|
|
||||||
description: Test Artifact Data Lake - Store and query test artifacts
|
|
||||||
type: application
|
|
||||||
version: 1.0.0
|
|
||||||
appVersion: "1.0.0"
|
|
||||||
keywords:
|
|
||||||
- testing
|
|
||||||
- artifacts
|
|
||||||
- storage
|
|
||||||
- data-lake
|
|
||||||
maintainers:
|
|
||||||
- name: Your Team
|
|
||||||
42
helm/README.md
Normal file
42
helm/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Helm Charts
|
||||||
|
|
||||||
|
This directory contains Helm charts for deploying Warehouse13.
|
||||||
|
|
||||||
|
## Current Chart (Recommended)
|
||||||
|
|
||||||
|
**Location:** `./warehouse13/`
|
||||||
|
|
||||||
|
The latest, fully-featured Helm chart with:
|
||||||
|
- Warehouse13 branding
|
||||||
|
- Configurable images for all components
|
||||||
|
- Multiple deployment scenarios (dev, production, air-gapped)
|
||||||
|
- Comprehensive documentation
|
||||||
|
- Example values files
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
helm install warehouse13 ./warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentation:** See [warehouse13/README.md](./warehouse13/README.md)
|
||||||
|
|
||||||
|
## Migration from Legacy Chart
|
||||||
|
|
||||||
|
If you were using an older version of the chart, migration is straightforward:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uninstall old chart (if named "datalake" or other name)
|
||||||
|
helm uninstall <old-release-name>
|
||||||
|
|
||||||
|
# Install new chart
|
||||||
|
helm install warehouse13 ./warehouse13 --namespace warehouse13 --create-namespace
|
||||||
|
|
||||||
|
# Or upgrade in place (if compatible)
|
||||||
|
helm upgrade <old-release-name> ./warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Check your values.yaml configuration and update image repositories, resource limits, and other settings as needed.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
See [../docs/HELM-DEPLOYMENT.md](../docs/HELM-DEPLOYMENT.md) for comprehensive deployment guide.
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "datalake.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "datalake.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
{{- if not .Values.autoscaling.enabled }}
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
{{- end }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "datalake.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "datalake.selectorLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "datalake.serviceAccountName" . }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.targetPort }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
env:
|
|
||||||
- name: DATABASE_URL
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
key: database-url
|
|
||||||
- name: STORAGE_BACKEND
|
|
||||||
value: {{ .Values.config.storageBackend | quote }}
|
|
||||||
- name: MAX_UPLOAD_SIZE
|
|
||||||
value: {{ .Values.config.maxUploadSize | quote }}
|
|
||||||
{{- if eq .Values.config.storageBackend "s3" }}
|
|
||||||
- name: AWS_ACCESS_KEY_ID
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
key: aws-access-key-id
|
|
||||||
- name: AWS_SECRET_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
key: aws-secret-access-key
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: {{ .Values.aws.region | quote }}
|
|
||||||
- name: S3_BUCKET_NAME
|
|
||||||
value: {{ .Values.aws.bucketName | quote }}
|
|
||||||
{{- else }}
|
|
||||||
- name: MINIO_ENDPOINT
|
|
||||||
value: "{{ include "datalake.fullname" . }}-minio:9000"
|
|
||||||
- name: MINIO_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
key: minio-access-key
|
|
||||||
- name: MINIO_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
key: minio-secret-key
|
|
||||||
- name: MINIO_BUCKET_NAME
|
|
||||||
value: "test-artifacts"
|
|
||||||
- name: MINIO_SECURE
|
|
||||||
value: "false"
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.env }}
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: {{ include "datalake.fullname" . }}-secrets
|
|
||||||
labels:
|
|
||||||
{{- include "datalake.labels" . | nindent 4 }}
|
|
||||||
type: Opaque
|
|
||||||
stringData:
|
|
||||||
database-url: "postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ include "datalake.fullname" . }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
|
|
||||||
{{- if .Values.aws.enabled }}
|
|
||||||
aws-access-key-id: {{ .Values.aws.accessKeyId | quote }}
|
|
||||||
aws-secret-access-key: {{ .Values.aws.secretAccessKey | quote }}
|
|
||||||
{{- else }}
|
|
||||||
minio-access-key: {{ .Values.minio.rootUser | quote }}
|
|
||||||
minio-secret-key: {{ .Values.minio.rootPassword | quote }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "datalake.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "datalake.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
{{- include "datalake.selectorLabels" . | nindent 4 }}
|
|
||||||
111
helm/values.yaml
111
helm/values.yaml
@@ -1,111 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: datalake
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
serviceAccount:
|
|
||||||
create: true
|
|
||||||
annotations: {}
|
|
||||||
name: ""
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
|
|
||||||
podSecurityContext:
|
|
||||||
fsGroup: 1000
|
|
||||||
|
|
||||||
securityContext:
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
readOnlyRootFilesystem: false
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 1000
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 8000
|
|
||||||
targetPort: 8000
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: false
|
|
||||||
className: ""
|
|
||||||
annotations: {}
|
|
||||||
hosts:
|
|
||||||
- host: datalake.local
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls: []
|
|
||||||
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 1000m
|
|
||||||
memory: 1Gi
|
|
||||||
requests:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
|
|
||||||
autoscaling:
|
|
||||||
enabled: false
|
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 10
|
|
||||||
targetCPUUtilizationPercentage: 80
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
|
|
||||||
tolerations: []
|
|
||||||
|
|
||||||
affinity: {}
|
|
||||||
|
|
||||||
# Application configuration
|
|
||||||
config:
|
|
||||||
storageBackend: minio # or "s3"
|
|
||||||
maxUploadSize: 524288000 # 500MB
|
|
||||||
|
|
||||||
# PostgreSQL configuration
|
|
||||||
postgresql:
|
|
||||||
enabled: true
|
|
||||||
auth:
|
|
||||||
username: user
|
|
||||||
password: password
|
|
||||||
database: datalake
|
|
||||||
primary:
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
size: 10Gi
|
|
||||||
|
|
||||||
# MinIO configuration (for self-hosted storage)
|
|
||||||
minio:
|
|
||||||
enabled: true
|
|
||||||
mode: standalone
|
|
||||||
rootUser: minioadmin
|
|
||||||
rootPassword: minioadmin
|
|
||||||
persistence:
|
|
||||||
enabled: true
|
|
||||||
size: 50Gi
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 9000
|
|
||||||
consoleService:
|
|
||||||
port: 9001
|
|
||||||
|
|
||||||
# AWS S3 configuration (when using AWS)
|
|
||||||
aws:
|
|
||||||
enabled: false
|
|
||||||
accessKeyId: ""
|
|
||||||
secretAccessKey: ""
|
|
||||||
region: us-east-1
|
|
||||||
bucketName: test-artifacts
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
env:
|
|
||||||
- name: API_HOST
|
|
||||||
value: "0.0.0.0"
|
|
||||||
- name: API_PORT
|
|
||||||
value: "8000"
|
|
||||||
23
helm/warehouse13/.helmignore
Normal file
23
helm/warehouse13/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
281
helm/warehouse13/ARCHITECTURE.md
Normal file
281
helm/warehouse13/ARCHITECTURE.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# Warehouse13 Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Warehouse13 uses a **unified application container** that includes both the frontend and backend in a single Docker image using a multi-stage build.
|
||||||
|
|
||||||
|
## Docker Build Strategy
|
||||||
|
|
||||||
|
### Multi-Stage Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Build Angular Frontend
|
||||||
|
FROM node:24-alpine AS frontend-build
|
||||||
|
WORKDIR /frontend
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build:prod
|
||||||
|
|
||||||
|
# Stage 2: Python Backend with Static Frontend
|
||||||
|
FROM python:3.11-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
# Copy backend code
|
||||||
|
COPY app/ ./app/
|
||||||
|
# Copy built frontend from stage 1
|
||||||
|
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
|
||||||
|
# Run FastAPI server
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
1. **Simplified Deployment** - Single container to manage
|
||||||
|
2. **Reduced Resource Usage** - No separate nginx container needed
|
||||||
|
3. **Easier Scaling** - Scale one deployment instead of two
|
||||||
|
4. **Consistent Versioning** - Frontend and backend versions always match
|
||||||
|
5. **Faster Deployments** - Fewer containers to orchestrate
|
||||||
|
|
||||||
|
## Service Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ warehouse13-app │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ FastAPI Backend (Port 8000) │ │
|
||||||
|
│ │ ├── /api/* → REST API │ │
|
||||||
|
│ │ ├── /health → Health check │ │
|
||||||
|
│ │ ├── /docs → API documentation │ │
|
||||||
|
│ │ └── /* → Angular SPA │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Static Files: /static/ │ │
|
||||||
|
│ │ └── Angular build output │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
├────────────────┐
|
||||||
|
↓ ↓
|
||||||
|
┌──────────┐ ┌────────────┐
|
||||||
|
│PostgreSQL│ │ MinIO │
|
||||||
|
│(Metadata)│ │ (Blobs) │
|
||||||
|
└──────────┘ └────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helm Chart Structure
|
||||||
|
|
||||||
|
### Single Application Deployment
|
||||||
|
|
||||||
|
The Helm chart creates:
|
||||||
|
|
||||||
|
1. **1 Deployment**: `warehouse13-app`
|
||||||
|
- Runs the unified container
|
||||||
|
- Configurable replicas (default: 2)
|
||||||
|
- Health checks on `/health` endpoint
|
||||||
|
|
||||||
|
2. **1 Service**: `warehouse13-app`
|
||||||
|
- Exposes port 8000
|
||||||
|
- Routes all traffic to the application
|
||||||
|
|
||||||
|
3. **Optional Ingress**
|
||||||
|
- All paths route to `warehouse13-app:8000`
|
||||||
|
- FastAPI handles routing internally
|
||||||
|
|
||||||
|
### Kubernetes Resources
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# warehouse13-app Deployment
|
||||||
|
- Replicas: 2 (configurable)
|
||||||
|
- Port: 8000
|
||||||
|
- Health checks: /health
|
||||||
|
- Environment: DATABASE_URL, MINIO_* vars
|
||||||
|
|
||||||
|
# warehouse13-app Service
|
||||||
|
- Type: ClusterIP
|
||||||
|
- Port: 8000 → 8000
|
||||||
|
|
||||||
|
# Ingress (optional)
|
||||||
|
- Path: / → warehouse13-app:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Image Configuration
|
||||||
|
|
||||||
|
In `values.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app:
|
||||||
|
enabled: true
|
||||||
|
image:
|
||||||
|
repository: warehouse13/app # Single unified image
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "384Mi" # Combined frontend + backend
|
||||||
|
cpu: "350m"
|
||||||
|
limits:
|
||||||
|
memory: "768Mi"
|
||||||
|
cpu: "750m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing the Application
|
||||||
|
|
||||||
|
**Via Port Forward:**
|
||||||
|
```bash
|
||||||
|
kubectl port-forward svc/warehouse13-app 8000:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access:
|
||||||
|
- Frontend: http://localhost:8000
|
||||||
|
- API: http://localhost:8000/api
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
- Health: http://localhost:8000/health
|
||||||
|
|
||||||
|
**Via Ingress:**
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: warehouse13.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend: app # All traffic to one service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Separate Services
|
||||||
|
|
||||||
|
If you previously had separate `api` and `frontend` deployments:
|
||||||
|
|
||||||
|
### Before (Old Architecture)
|
||||||
|
```yaml
|
||||||
|
# values.yaml (old)
|
||||||
|
api:
|
||||||
|
image: warehouse13/api
|
||||||
|
replicas: 2
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: warehouse13/frontend
|
||||||
|
replicas: 2
|
||||||
|
|
||||||
|
# Two deployments, two services
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Current Architecture)
|
||||||
|
```yaml
|
||||||
|
# values.yaml (current)
|
||||||
|
app:
|
||||||
|
image: warehouse13/app # Unified image
|
||||||
|
replicas: 2
|
||||||
|
|
||||||
|
# One deployment, one service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Steps
|
||||||
|
|
||||||
|
1. **Update values.yaml** - Change from `api`/`frontend` to `app`
|
||||||
|
2. **Update image references** - Use `warehouse13/app` instead of separate images
|
||||||
|
3. **Update ingress** - Point all paths to `app` backend
|
||||||
|
4. **Deploy** - Helm will handle the transition
|
||||||
|
5. **Verify** - Check that both frontend and API work through single service
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Building the Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build unified image
|
||||||
|
docker build -t warehouse13/app:dev .
|
||||||
|
|
||||||
|
# Or for air-gapped environments with custom registry
|
||||||
|
docker build \
|
||||||
|
--build-arg NPM_REGISTRY=https://registry.npmjs.org/ \
|
||||||
|
-t warehouse13/app:v1.0.0 .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8000:8000 \
|
||||||
|
-e DATABASE_URL=postgresql://user:pass@host/db \
|
||||||
|
-e MINIO_ENDPOINT=minio:9000 \
|
||||||
|
warehouse13/app:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- Frontend: http://localhost:8000
|
||||||
|
- API: http://localhost:8000/docs
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Resource Allocation
|
||||||
|
|
||||||
|
The unified container combines both frontend serving and API processing:
|
||||||
|
|
||||||
|
- **Memory**: Angular assets (~50MB) + Python runtime (~100MB) + working memory
|
||||||
|
- **CPU**: Primarily used for API requests; static file serving is lightweight
|
||||||
|
- **Recommended Minimum**: 384Mi memory, 350m CPU
|
||||||
|
- **Production**: 768Mi memory, 750m CPU per replica
|
||||||
|
|
||||||
|
### Scaling Strategy
|
||||||
|
|
||||||
|
Scale horizontally by increasing replicas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scale to 5 replicas
|
||||||
|
kubectl scale deployment warehouse13-app --replicas=5
|
||||||
|
|
||||||
|
# Or via Helm
|
||||||
|
helm upgrade warehouse13 ./helm/warehouse13 --set app.replicas=5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
FastAPI serves static files efficiently with:
|
||||||
|
- ETag support
|
||||||
|
- Browser caching headers
|
||||||
|
- Gzip compression (if enabled in FastAPI config)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Frontend Not Loading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if static files exist in container
|
||||||
|
kubectl exec -it warehouse13-app-xxx -- ls -la /app/static/
|
||||||
|
|
||||||
|
# Should see: index.html, *.js, *.css files
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Not Working
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check API health
|
||||||
|
kubectl exec -it warehouse13-app-xxx -- curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
kubectl logs warehouse13-app-xxx -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Both Frontend and API Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if app is running
|
||||||
|
kubectl get pods -l app.kubernetes.io/component=app
|
||||||
|
|
||||||
|
# Check service
|
||||||
|
kubectl get svc warehouse13-app
|
||||||
|
|
||||||
|
# Test connectivity
|
||||||
|
kubectl port-forward svc/warehouse13-app 8000:8000
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The unified architecture simplifies deployment and operations while maintaining the same functionality. All routing, caching, and API requests are handled by a single FastAPI application that serves both the Angular SPA and the REST API endpoints.
|
||||||
16
helm/warehouse13/Chart.yaml
Normal file
16
helm/warehouse13/Chart.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: warehouse13
|
||||||
|
description: Warehouse13 - Enterprise Test Artifact Storage
|
||||||
|
type: application
|
||||||
|
version: 1.0.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
|
keywords:
|
||||||
|
- testing
|
||||||
|
- artifacts
|
||||||
|
- storage
|
||||||
|
- datalake
|
||||||
|
maintainers:
|
||||||
|
- name: Warehouse13 Team
|
||||||
|
home: https://github.com/yourusername/warehouse13
|
||||||
|
sources:
|
||||||
|
- https://github.com/yourusername/warehouse13
|
||||||
148
helm/warehouse13/QUICKSTART.md
Normal file
148
helm/warehouse13/QUICKSTART.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Warehouse13 Helm Chart - Quick Start
|
||||||
|
|
||||||
|
## 5-Minute Deployment
|
||||||
|
|
||||||
|
### Prerequisites Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify Kubernetes cluster access
|
||||||
|
kubectl cluster-info
|
||||||
|
|
||||||
|
# Verify Helm is installed
|
||||||
|
helm version
|
||||||
|
|
||||||
|
# Create namespace
|
||||||
|
kubectl create namespace warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy with Defaults
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install chart
|
||||||
|
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13
|
||||||
|
|
||||||
|
# Wait for ready
|
||||||
|
kubectl wait --for=condition=ready pod --all -n warehouse13 --timeout=5m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In separate terminals, run:
|
||||||
|
|
||||||
|
# Terminal 1: Frontend
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
|
||||||
|
|
||||||
|
# Terminal 2: API
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
|
||||||
|
|
||||||
|
# Terminal 3: MinIO Console
|
||||||
|
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open in browser:
|
||||||
|
- **Frontend:** http://localhost:4200
|
||||||
|
- **API Docs:** http://localhost:8000/docs
|
||||||
|
- **MinIO Console:** http://localhost:9001
|
||||||
|
- Username: `minioadmin`
|
||||||
|
- Password: `minioadmin`
|
||||||
|
|
||||||
|
## Common Scenarios
|
||||||
|
|
||||||
|
### 1. Development (No Persistence)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--values ./helm/warehouse13/values-dev.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Production (With Ingress)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update values-production.yaml with your settings first
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--values ./helm/warehouse13/values-production.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Air-Gapped (Custom Registry)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update values-airgapped.yaml with your registry first
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--values ./helm/warehouse13/values-airgapped.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Custom Image Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--set postgres.image.repository=myregistry.com/postgres \
|
||||||
|
--set minio.image.repository=myregistry.com/minio \
|
||||||
|
--set api.image.repository=myregistry.com/warehouse13-api \
|
||||||
|
--set frontend.image.repository=myregistry.com/warehouse13-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check pods
|
||||||
|
kubectl get pods -n warehouse13
|
||||||
|
|
||||||
|
# Check services
|
||||||
|
kubectl get svc -n warehouse13
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=50
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
kubectl top pods -n warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uninstall release
|
||||||
|
helm uninstall warehouse13 -n warehouse13
|
||||||
|
|
||||||
|
# Delete PVCs (data will be lost!)
|
||||||
|
kubectl delete pvc -n warehouse13 --all
|
||||||
|
|
||||||
|
# Delete namespace
|
||||||
|
kubectl delete namespace warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Full Documentation:** [README.md](./README.md)
|
||||||
|
- **Deployment Guide:** [../../docs/HELM-DEPLOYMENT.md](../../docs/HELM-DEPLOYMENT.md)
|
||||||
|
- **Configuration Options:** [values.yaml](./values.yaml)
|
||||||
|
- **Example Configs:** [values-dev.yaml](./values-dev.yaml), [values-production.yaml](./values-production.yaml), [values-airgapped.yaml](./values-airgapped.yaml)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pods stuck in Pending
|
||||||
|
```bash
|
||||||
|
kubectl describe pod <pod-name> -n warehouse13
|
||||||
|
# Check: PVC status, node resources, storage classes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image pull errors
|
||||||
|
```bash
|
||||||
|
kubectl describe pod <pod-name> -n warehouse13
|
||||||
|
# Check: Image repository, credentials, network access
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection errors
|
||||||
|
```bash
|
||||||
|
kubectl logs -n warehouse13 warehouse13-postgres-0
|
||||||
|
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||||
|
- Documentation: https://warehouse13.example.com/docs
|
||||||
441
helm/warehouse13/README.md
Normal file
441
helm/warehouse13/README.md
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
# Warehouse13 Helm Chart
|
||||||
|
|
||||||
|
Enterprise Test Artifact Storage - Kubernetes deployment via Helm
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This Helm chart deploys the complete Warehouse13 stack on Kubernetes:
|
||||||
|
|
||||||
|
- **PostgreSQL 15** - Metadata database
|
||||||
|
- **MinIO** - S3-compatible object storage
|
||||||
|
- **FastAPI Backend** - REST API server
|
||||||
|
- **Angular Frontend** - Web UI (nginx-served)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes 1.19+
|
||||||
|
- Helm 3.0+
|
||||||
|
- PV provisioner support (for persistent storage)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the Warehouse13 chart repository (if published)
|
||||||
|
helm repo add warehouse13 https://charts.warehouse13.example.com
|
||||||
|
helm repo update
|
||||||
|
|
||||||
|
# Install with default values
|
||||||
|
helm install my-warehouse13 warehouse13/warehouse13
|
||||||
|
|
||||||
|
# Or install from local chart
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install with custom values
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 \
|
||||||
|
--set postgres.persistence.size=20Gi \
|
||||||
|
--set minio.persistence.size=100Gi \
|
||||||
|
--set api.replicas=3
|
||||||
|
|
||||||
|
# Install in a specific namespace
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 \
|
||||||
|
--namespace warehouse13 \
|
||||||
|
--create-namespace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Configurable Images
|
||||||
|
|
||||||
|
All component images can be customized via values.yaml or command-line flags:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgres:
|
||||||
|
image:
|
||||||
|
repository: postgres
|
||||||
|
tag: 15-alpine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image:
|
||||||
|
repository: minio/minio
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
api:
|
||||||
|
image:
|
||||||
|
repository: warehouse13/api
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image:
|
||||||
|
repository: warehouse13/frontend
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Using custom image registry**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 \
|
||||||
|
--set postgres.image.repository=myregistry.example.com/postgres \
|
||||||
|
--set minio.image.repository=myregistry.example.com/minio \
|
||||||
|
--set api.image.repository=myregistry.example.com/warehouse13-api \
|
||||||
|
--set frontend.image.repository=myregistry.example.com/warehouse13-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Air-gapped deployment with specific tags**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 \
|
||||||
|
--set postgres.image.repository=harbor.internal/library/postgres \
|
||||||
|
--set postgres.image.tag=15-alpine \
|
||||||
|
--set minio.image.repository=harbor.internal/library/minio \
|
||||||
|
--set minio.image.tag=RELEASE.2024-01-01T00-00-00Z \
|
||||||
|
--set api.image.repository=harbor.internal/warehouse13/api \
|
||||||
|
--set api.image.tag=v1.0.0 \
|
||||||
|
--set frontend.image.repository=harbor.internal/warehouse13/frontend \
|
||||||
|
--set frontend.image.tag=v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Parameters
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|-----------|-------------|---------|
|
||||||
|
| `global.deploymentMode` | Deployment mode (standard/airgapped) | `standard` |
|
||||||
|
| `global.storageBackend` | Storage backend (minio/s3) | `minio` |
|
||||||
|
| `postgres.persistence.enabled` | Enable PostgreSQL persistence | `true` |
|
||||||
|
| `postgres.persistence.size` | PostgreSQL PVC size | `10Gi` |
|
||||||
|
| `postgres.auth.username` | PostgreSQL username | `user` |
|
||||||
|
| `postgres.auth.password` | PostgreSQL password | `password` |
|
||||||
|
| `minio.persistence.enabled` | Enable MinIO persistence | `true` |
|
||||||
|
| `minio.persistence.size` | MinIO PVC size | `50Gi` |
|
||||||
|
| `minio.auth.rootUser` | MinIO root username | `minioadmin` |
|
||||||
|
| `minio.auth.rootPassword` | MinIO root password | `minioadmin` |
|
||||||
|
| `api.replicas` | Number of API replicas | `2` |
|
||||||
|
| `frontend.replicas` | Number of frontend replicas | `2` |
|
||||||
|
| `ingress.enabled` | Enable ingress | `false` |
|
||||||
|
| `ingress.className` | Ingress class name | `nginx` |
|
||||||
|
| `ingress.hosts` | Ingress hosts configuration | See values.yaml |
|
||||||
|
|
||||||
|
### Example Configurations
|
||||||
|
|
||||||
|
#### Production with Ingress
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# values-production.yaml
|
||||||
|
global:
|
||||||
|
deploymentMode: "standard"
|
||||||
|
storageBackend: "minio"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
persistence:
|
||||||
|
size: 50Gi
|
||||||
|
storageClass: "fast-ssd"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
persistence:
|
||||||
|
size: 500Gi
|
||||||
|
storageClass: "bulk-storage"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
|
||||||
|
api:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
replicas: 3
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "nginx"
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
hosts:
|
||||||
|
- host: warehouse13.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend: frontend
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
backend: api
|
||||||
|
tls:
|
||||||
|
- secretName: warehouse13-tls
|
||||||
|
hosts:
|
||||||
|
- warehouse13.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 -f values-production.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Air-Gapped Environment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# values-airgapped.yaml
|
||||||
|
global:
|
||||||
|
deploymentMode: "airgapped"
|
||||||
|
storageBackend: "minio"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image:
|
||||||
|
repository: harbor.internal.example.com/library/postgres
|
||||||
|
tag: 15-alpine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image:
|
||||||
|
repository: harbor.internal.example.com/library/minio
|
||||||
|
tag: RELEASE.2024-01-01T00-00-00Z
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
api:
|
||||||
|
image:
|
||||||
|
repository: harbor.internal.example.com/warehouse13/api
|
||||||
|
tag: v1.0.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image:
|
||||||
|
repository: harbor.internal.example.com/warehouse13/frontend
|
||||||
|
tag: v1.0.0
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 -f values-airgapped.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development/Testing
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# values-dev.yaml
|
||||||
|
global:
|
||||||
|
deploymentMode: "standard"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
persistence:
|
||||||
|
enabled: false # Use emptyDir for quick testing
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
persistence:
|
||||||
|
enabled: false
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
|
||||||
|
api:
|
||||||
|
replicas: 1
|
||||||
|
image:
|
||||||
|
tag: dev
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
replicas: 1
|
||||||
|
image:
|
||||||
|
tag: dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 -f values-dev.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing the Application
|
||||||
|
|
||||||
|
### Port Forwarding (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access frontend
|
||||||
|
kubectl port-forward svc/warehouse13-frontend 4200:80
|
||||||
|
|
||||||
|
# Access API
|
||||||
|
kubectl port-forward svc/warehouse13-api 8000:8000
|
||||||
|
|
||||||
|
# Access MinIO console
|
||||||
|
kubectl port-forward svc/warehouse13-minio 9001:9001
|
||||||
|
|
||||||
|
# Then visit:
|
||||||
|
# - Frontend: http://localhost:4200
|
||||||
|
# - API: http://localhost:8000
|
||||||
|
# - MinIO Console: http://localhost:9001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via Ingress (Production)
|
||||||
|
|
||||||
|
If ingress is enabled:
|
||||||
|
```
|
||||||
|
https://warehouse13.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upgrade with new values
|
||||||
|
helm upgrade my-warehouse13 ./helm/warehouse13 \
|
||||||
|
--set api.image.tag=v2.0.0 \
|
||||||
|
--set frontend.image.tag=v2.0.0
|
||||||
|
|
||||||
|
# Upgrade with values file
|
||||||
|
helm upgrade my-warehouse13 ./helm/warehouse13 -f values-production.yaml
|
||||||
|
|
||||||
|
# Upgrade and wait for completion
|
||||||
|
helm upgrade my-warehouse13 ./helm/warehouse13 --wait --timeout 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstalling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uninstall the release
|
||||||
|
helm uninstall my-warehouse13
|
||||||
|
|
||||||
|
# Note: PVCs are not deleted automatically. To delete them:
|
||||||
|
kubectl delete pvc -l app.kubernetes.io/instance=my-warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup and Restore
|
||||||
|
|
||||||
|
### PostgreSQL Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
kubectl exec -it warehouse13-postgres-0 -- pg_dump -U user warehouse13 > backup.sql
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
kubectl exec -i warehouse13-postgres-0 -- psql -U user warehouse13 < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### MinIO Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install mc (MinIO Client)
|
||||||
|
# Configure mc alias
|
||||||
|
mc alias set w13 http://localhost:9001 minioadmin minioadmin
|
||||||
|
|
||||||
|
# Mirror bucket
|
||||||
|
mc mirror w13/artifacts ./backup/artifacts
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
mc mirror ./backup/artifacts w13/artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check Pod Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods -l app.kubernetes.io/name=warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# API logs
|
||||||
|
kubectl logs -l app.kubernetes.io/component=api -f
|
||||||
|
|
||||||
|
# Frontend logs
|
||||||
|
kubectl logs -l app.kubernetes.io/component=frontend -f
|
||||||
|
|
||||||
|
# PostgreSQL logs
|
||||||
|
kubectl logs warehouse13-postgres-0 -f
|
||||||
|
|
||||||
|
# MinIO logs
|
||||||
|
kubectl logs warehouse13-minio-0 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get svc -l app.kubernetes.io/name=warehouse13
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Pods stuck in Pending**
|
||||||
|
- Check PVC status: `kubectl get pvc`
|
||||||
|
- Verify storage class exists: `kubectl get storageclass`
|
||||||
|
- Check node resources: `kubectl describe nodes`
|
||||||
|
|
||||||
|
**Database connection errors**
|
||||||
|
- Verify postgres pod is running: `kubectl get pod warehouse13-postgres-0`
|
||||||
|
- Check database logs: `kubectl logs warehouse13-postgres-0`
|
||||||
|
- Verify secret exists: `kubectl get secret warehouse13-secrets`
|
||||||
|
|
||||||
|
**Frontend cannot reach API**
|
||||||
|
- Check ingress configuration: `kubectl describe ingress warehouse13-ingress`
|
||||||
|
- Verify API service: `kubectl get svc warehouse13-api`
|
||||||
|
- Check API pod health: `kubectl get pods -l app.kubernetes.io/component=api`
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
|
||||||
|
**Default credentials are for development only!** In production:
|
||||||
|
|
||||||
|
1. **Use external secrets management:**
|
||||||
|
```yaml
|
||||||
|
# Use sealed-secrets, external-secrets, or similar
|
||||||
|
postgres:
|
||||||
|
auth:
|
||||||
|
username: "{{ .Values.externalSecrets.postgresUser }}"
|
||||||
|
password: "{{ .Values.externalSecrets.postgresPassword }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Or create secrets manually:**
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic warehouse13-secrets \
|
||||||
|
--from-literal=postgres-username=secure-user \
|
||||||
|
--from-literal=postgres-password=secure-password \
|
||||||
|
--from-literal=minio-root-user=secure-minio-user \
|
||||||
|
--from-literal=minio-root-password=secure-minio-password
|
||||||
|
|
||||||
|
# Then install without default secrets
|
||||||
|
helm install my-warehouse13 ./helm/warehouse13 --set createSecrets=false
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Enable TLS:**
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
tls:
|
||||||
|
- secretName: warehouse13-tls
|
||||||
|
hosts:
|
||||||
|
- warehouse13.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
|
||||||
|
- Documentation: https://warehouse13.example.com/docs
|
||||||
131
helm/warehouse13/templates/NOTES.txt
Normal file
131
helm/warehouse13/templates/NOTES.txt
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
_ _ _ _ _____
|
||||||
|
| | | | | | / |___ /
|
||||||
|
| | | | __ _ _ __ ___| |__ ___ _ _ ___ / / |_ \
|
||||||
|
| |/\| |/ _` | '__/ _ \ '_ \ / _ \| | | / __|/ / ___) |
|
||||||
|
\ /\ / (_| | | | __/ | | | (_) | |_| \__ \_/ |____/
|
||||||
|
\/ \/ \__,_|_| \___|_| |_|\___/ \__,_|___(_)
|
||||||
|
|
||||||
|
Enterprise Test Artifact Storage has been deployed!
|
||||||
|
|
||||||
|
Chart Name: {{ .Chart.Name }}
|
||||||
|
Chart Version: {{ .Chart.Version }}
|
||||||
|
App Version: {{ .Chart.AppVersion }}
|
||||||
|
|
||||||
|
Release Name: {{ .Release.Name }}
|
||||||
|
Namespace: {{ .Release.Namespace }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
DEPLOYMENT INFORMATION:
|
||||||
|
|
||||||
|
{{- if .Values.app.enabled }}
|
||||||
|
Application (Unified API + Frontend):
|
||||||
|
Service: warehouse13-app
|
||||||
|
Replicas: {{ .Values.app.replicas }}
|
||||||
|
Image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}
|
||||||
|
Port: {{ .Values.app.service.port }}
|
||||||
|
Note: Multi-stage build includes both Angular frontend and FastAPI backend
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if .Values.postgres.enabled }}
|
||||||
|
PostgreSQL:
|
||||||
|
Service: warehouse13-postgres
|
||||||
|
Image: {{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}
|
||||||
|
Persistence: {{ if .Values.postgres.persistence.enabled }}Enabled ({{ .Values.postgres.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if .Values.minio.enabled }}
|
||||||
|
MinIO:
|
||||||
|
Service: warehouse13-minio
|
||||||
|
Image: {{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}
|
||||||
|
Persistence: {{ if .Values.minio.persistence.enabled }}Enabled ({{ .Values.minio.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ACCESSING YOUR APPLICATION:
|
||||||
|
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
|
||||||
|
1. Via Ingress:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
https://{{ .host }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- else }}
|
||||||
|
|
||||||
|
1. Using Port Forwarding:
|
||||||
|
|
||||||
|
# Application (Frontend + API)
|
||||||
|
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-app 8000:8000
|
||||||
|
Then visit:
|
||||||
|
- Frontend: http://localhost:8000
|
||||||
|
- API Docs: http://localhost:8000/docs
|
||||||
|
- Health: http://localhost:8000/health
|
||||||
|
|
||||||
|
# MinIO Console
|
||||||
|
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-minio 9001:9001
|
||||||
|
Then visit: http://localhost:9001
|
||||||
|
Username: {{ .Values.minio.auth.rootUser }}
|
||||||
|
Password: {{ .Values.minio.auth.rootPassword }}
|
||||||
|
|
||||||
|
2. Expose via LoadBalancer or Ingress for external access.
|
||||||
|
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
CHECKING STATUS:
|
||||||
|
|
||||||
|
# View all pods
|
||||||
|
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||||
|
|
||||||
|
# Check services
|
||||||
|
kubectl get svc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=app -f
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UPGRADING:
|
||||||
|
|
||||||
|
helm upgrade {{ .Release.Name }} warehouse13/warehouse13 \
|
||||||
|
--namespace {{ .Release.Namespace }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UNINSTALLING:
|
||||||
|
|
||||||
|
helm uninstall {{ .Release.Name }} --namespace {{ .Release.Namespace }}
|
||||||
|
|
||||||
|
# Note: PVCs are retained. To delete them:
|
||||||
|
kubectl delete pvc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{{- if not .Values.ingress.enabled }}
|
||||||
|
⚠️ IMPORTANT: Ingress is disabled. Enable it for production use:
|
||||||
|
--set ingress.enabled=true
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if eq .Values.postgres.auth.password "password" }}
|
||||||
|
⚠️ WARNING: Using default PostgreSQL password!
|
||||||
|
For production, set a secure password:
|
||||||
|
--set postgres.auth.password=YOUR_SECURE_PASSWORD
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if eq .Values.minio.auth.rootPassword "minioadmin" }}
|
||||||
|
⚠️ WARNING: Using default MinIO password!
|
||||||
|
For production, set a secure password:
|
||||||
|
--set minio.auth.rootPassword=YOUR_SECURE_PASSWORD
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more information, visit:
|
||||||
|
Documentation: https://github.com/yourusername/warehouse13
|
||||||
|
Issues: https://github.com/yourusername/warehouse13/issues
|
||||||
|
|
||||||
|
Thank you for using Warehouse13!
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
{{/*
|
{{/*
|
||||||
Expand the name of the chart.
|
Expand the name of the chart.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.name" -}}
|
{{- define "warehouse13.name" -}}
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Create a default fully qualified app name.
|
Create a default fully qualified app name.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.fullname" -}}
|
{{- define "warehouse13.fullname" -}}
|
||||||
{{- if .Values.fullnameOverride }}
|
{{- if .Values.fullnameOverride }}
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
|
|||||||
{{/*
|
{{/*
|
||||||
Create chart name and version as used by the chart label.
|
Create chart name and version as used by the chart label.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.chart" -}}
|
{{- define "warehouse13.chart" -}}
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Common labels
|
Common labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.labels" -}}
|
{{- define "warehouse13.labels" -}}
|
||||||
helm.sh/chart: {{ include "datalake.chart" . }}
|
helm.sh/chart: {{ include "warehouse13.chart" . }}
|
||||||
{{ include "datalake.selectorLabels" . }}
|
{{ include "warehouse13.selectorLabels" . }}
|
||||||
{{- if .Chart.AppVersion }}
|
{{- if .Chart.AppVersion }}
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -43,18 +43,29 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|||||||
{{/*
|
{{/*
|
||||||
Selector labels
|
Selector labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.selectorLabels" -}}
|
{{- define "warehouse13.selectorLabels" -}}
|
||||||
app.kubernetes.io/name: {{ include "datalake.name" . }}
|
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Create the name of the service account to use
|
Create the name of the service account to use
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "datalake.serviceAccountName" -}}
|
{{- define "warehouse13.serviceAccountName" -}}
|
||||||
{{- if .Values.serviceAccount.create }}
|
{{- if .Values.serviceAccount.create }}
|
||||||
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
|
{{- default (include "warehouse13.fullname" .) .Values.serviceAccount.name }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
PostgreSQL connection string
|
||||||
|
*/}}
|
||||||
|
{{- define "warehouse13.postgresUrl" -}}
|
||||||
|
{{- if .Values.app.env.databaseUrl }}
|
||||||
|
{{- .Values.app.env.databaseUrl }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "postgresql://%s:%s@warehouse13-postgres:%d/%s" .Values.postgres.auth.username .Values.postgres.auth.password (.Values.postgres.service.port | int) .Values.postgres.auth.database }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
99
helm/warehouse13/templates/app-deployment.yaml
Normal file
99
helm/warehouse13/templates/app-deployment.yaml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{{- if .Values.app.enabled }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: warehouse13-app
|
||||||
|
labels:
|
||||||
|
{{- include "warehouse13.labels" . | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: app
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.app.replicas }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
|
||||||
|
app.kubernetes.io/component: app
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
|
||||||
|
app.kubernetes.io/component: app
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitlab-dev-ns-registry-secret
|
||||||
|
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||||
|
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8000
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: warehouse13-secrets
|
||||||
|
key: database-url
|
||||||
|
- name: STORAGE_BACKEND
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: warehouse13-config
|
||||||
|
key: STORAGE_BACKEND
|
||||||
|
- name: MINIO_ENDPOINT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: warehouse13-config
|
||||||
|
key: MINIO_ENDPOINT
|
||||||
|
- name: MINIO_ACCESS_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: warehouse13-secrets
|
||||||
|
key: minio-root-user
|
||||||
|
- name: MINIO_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: warehouse13-secrets
|
||||||
|
key: minio-root-password
|
||||||
|
- name: MINIO_BUCKET_NAME
|
||||||
|
value: "test-artifacts"
|
||||||
|
- name: MINIO_SECURE
|
||||||
|
value: "false"
|
||||||
|
- name: DEPLOYMENT_MODE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: warehouse13-config
|
||||||
|
key: DEPLOYMENT_MODE
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.app.resources | nindent 10 }}
|
||||||
|
{{- if .Values.app.healthCheck.enabled }}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.app.healthCheck.liveness.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.app.healthCheck.liveness.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.app.healthCheck.liveness.periodSeconds }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: {{ .Values.app.healthCheck.readiness.path }}
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.app.healthCheck.readiness.initialDelaySeconds }}
|
||||||
|
periodSeconds: {{ .Values.app.healthCheck.readiness.periodSeconds }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user