Initial push of Plunk Next
This commit is contained in:
+66
-5
@@ -1,6 +1,67 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
out
|
||||
|
||||
# Development
|
||||
.env
|
||||
.next/
|
||||
.github/
|
||||
dist/
|
||||
assets/
|
||||
node_modules/
|
||||
.env*.local
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
**/__tests__
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
**/*.test.js
|
||||
**/*.test.jsx
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
**/*.spec.js
|
||||
**/*.spec.jsx
|
||||
test/
|
||||
vitest.config.ts
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
|
||||
# Documentation (not needed in image)
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker
|
||||
!docker/nginx
|
||||
.dockerignore
|
||||
|
||||
# Misc
|
||||
.cursor
|
||||
.eslintcache
|
||||
.cache
|
||||
tmp
|
||||
temp
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# ========================================
|
||||
# Plunk Self-Hosting Configuration
|
||||
# ========================================
|
||||
|
||||
# ========================================
|
||||
# REQUIRED: Security & Database
|
||||
# ========================================
|
||||
DB_PASSWORD=changeme123
|
||||
JWT_SECRET=
|
||||
|
||||
# ========================================
|
||||
# REQUIRED: Domains
|
||||
# ========================================
|
||||
# Replace example.com with your domain
|
||||
# Or use *.localhost for local testing
|
||||
API_DOMAIN=api.example.com
|
||||
DASHBOARD_DOMAIN=app.example.com
|
||||
LANDING_DOMAIN=www.example.com
|
||||
WIKI_DOMAIN=docs.example.com
|
||||
|
||||
# Set to 'true' if using HTTPS in production (behind a reverse proxy/load balancer)
|
||||
# This affects how application URIs are auto-generated from domain names
|
||||
USE_HTTPS=false
|
||||
|
||||
# ========================================
|
||||
# REQUIRED: AWS SES (Email Sending)
|
||||
# ========================================
|
||||
AWS_SES_REGION=us-east-1
|
||||
AWS_SES_ACCESS_KEY_ID=
|
||||
AWS_SES_SECRET_ACCESS_KEY=
|
||||
|
||||
# Configuration sets for email tracking
|
||||
# SES_CONFIGURATION_SET: Default configuration with open/click tracking enabled
|
||||
SES_CONFIGURATION_SET=plunk-configuration-set
|
||||
|
||||
# SES_CONFIGURATION_SET_NO_TRACKING: Optional configuration without tracking
|
||||
# If not set, the tracking toggle will be hidden in project settings
|
||||
# When set, projects can choose to disable email tracking
|
||||
SES_CONFIGURATION_SET_NO_TRACKING=plunk-no-tracking-configuration-set
|
||||
|
||||
# ========================================
|
||||
# OPTIONAL: OAuth Login
|
||||
# ========================================
|
||||
GITHUB_OAUTH_CLIENT=
|
||||
GITHUB_OAUTH_SECRET=
|
||||
GOOGLE_OAUTH_CLIENT=
|
||||
GOOGLE_OAUTH_SECRET=
|
||||
|
||||
# ========================================
|
||||
# OPTIONAL: Stripe Billing
|
||||
# ========================================
|
||||
STRIPE_SK=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRICE_ONBOARDING=
|
||||
STRIPE_PRICE_EMAIL_USAGE=
|
||||
STRIPE_METER_EVENT_NAME=emails
|
||||
|
||||
# ========================================
|
||||
# OPTIONAL: File Storage (Minio)
|
||||
# ========================================
|
||||
# Minio is included by default in Docker Compose
|
||||
# These credentials match the Minio service configuration
|
||||
# Leave defaults unless you're using external storage
|
||||
MINIO_ROOT_USER=plunk
|
||||
MINIO_ROOT_PASSWORD=plunkminiopass
|
||||
MINIO_API_PORT=9000
|
||||
MINIO_CONSOLE_PORT=9001
|
||||
|
||||
# S3-compatible storage configuration
|
||||
# For self-hosted: uses internal Minio service (defaults work out of the box)
|
||||
S3_ENDPOINT=http://minio:9000
|
||||
S3_ACCESS_KEY_ID=plunk
|
||||
S3_ACCESS_KEY_SECRET=plunkminiopass
|
||||
S3_BUCKET=uploads
|
||||
S3_PUBLIC_URL=http://localhost:9000/uploads
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# ========================================
|
||||
# OPTIONAL: SMTP Server
|
||||
# ========================================
|
||||
# The SMTP relay server allows sending emails via SMTP protocol
|
||||
# TLS certificates can be mounted via:
|
||||
# 1. Traefik acme.json (requires SMTP_DOMAIN to select the right cert)
|
||||
# 2. PEM files (privkey.pem and fullchain.pem)
|
||||
|
||||
# SMTP domain - Required if using Traefik acme.json with multiple certificates
|
||||
# Optional if using PEM files
|
||||
SMTP_DOMAIN=smtp.example.com
|
||||
|
||||
# SMTP Ports (defaults work for most setups)
|
||||
# PORT_SECURE=465 # SMTPS (implicit TLS)
|
||||
# PORT_SUBMISSION=587 # SMTP Submission (STARTTLS)
|
||||
|
||||
# Maximum recipients per email (default: 5)
|
||||
# MAX_RECIPIENTS=5
|
||||
|
||||
# ========================================
|
||||
# ADVANCED (rarely needed)
|
||||
# ========================================
|
||||
# NGINX_PORT=80
|
||||
@@ -0,0 +1,45 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:@typescript-eslint/stylistic-type-checked',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'next/core-web-vitals',
|
||||
'prettier',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{argsIgnorePattern: '^_', varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': ['warn', {prefer: 'type-imports'}],
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling'],
|
||||
'newlines-between': 'always',
|
||||
'alphabetize': {order: 'asc'},
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules/', 'dist/', '.next/', '*.config.js', '*.config.ts'],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
## Description
|
||||
|
||||
<!-- Describe your changes in detail -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
<!-- Mark the appropriate option with an 'x' -->
|
||||
|
||||
- [ ] `feat:` New feature (MINOR version bump)
|
||||
- [ ] `fix:` Bug fix (PATCH version bump)
|
||||
- [ ] `feat!:` Breaking change - new feature (MAJOR version bump)
|
||||
- [ ] `fix!:` Breaking change - bug fix (MAJOR version bump)
|
||||
- [ ] `docs:` Documentation update (no version bump)
|
||||
- [ ] `chore:` Maintenance/dependencies (no version bump)
|
||||
- [ ] `refactor:` Code refactoring (no version bump)
|
||||
- [ ] `test:` Adding tests (no version bump)
|
||||
- [ ] `perf:` Performance improvement (PATCH version bump)
|
||||
|
||||
## PR Title Format
|
||||
|
||||
<!--
|
||||
Your PR title should follow conventional commits format:
|
||||
✅ feat: add email template editor
|
||||
✅ fix: resolve memory leak in worker
|
||||
✅ feat!: redesign API authentication
|
||||
❌ Added new feature (missing type prefix)
|
||||
|
||||
See VERSIONING.md for more details
|
||||
-->
|
||||
|
||||
## Testing
|
||||
|
||||
<!-- Describe how you tested your changes -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] PR title follows conventional commits format
|
||||
- [ ] Code builds successfully
|
||||
- [ ] Tests pass locally
|
||||
- [ ] Documentation updated (if needed)
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Link any related issues here -->
|
||||
Closes #
|
||||
@@ -0,0 +1,197 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- next
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Service containers for database, Redis, and Minio
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: plunk_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
minio:
|
||||
image: minio/minio:edge-cicd
|
||||
env:
|
||||
MINIO_ROOT_USER: plunk
|
||||
MINIO_ROOT_PASSWORD: plunkminiopass
|
||||
ports:
|
||||
- 9000:9000
|
||||
options: >-
|
||||
--health-cmd "curl -f http://localhost:9000/minio/health/live || exit 1"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup environment variables
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
NODE_ENV=test
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
DIRECT_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET=test-jwt-secret-for-ci-only
|
||||
|
||||
# S3 (Minio)
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
S3_ACCESS_KEY_ID=plunk
|
||||
S3_ACCESS_KEY_SECRET=plunkminiopass
|
||||
S3_BUCKET=uploads
|
||||
S3_PUBLIC_URL=http://localhost:9000/uploads
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# Application URLs (not needed for tests but required by schema)
|
||||
API_URI=http://localhost:8080
|
||||
DASHBOARD_URI=http://localhost:3000
|
||||
LANDING_URI=http://localhost:4000
|
||||
WIKI_URI=http://localhost:1000
|
||||
|
||||
# AWS SES (mock values for tests)
|
||||
AWS_SES_REGION=us-east-1
|
||||
AWS_SES_ACCESS_KEY_ID=mock
|
||||
AWS_SES_SECRET_ACCESS_KEY=mock
|
||||
SES_CONFIGURATION_SET=test
|
||||
SES_CONFIGURATION_SET_NO_TRACKING=test-no-tracking
|
||||
EOF
|
||||
|
||||
- name: Build shared packages
|
||||
run: yarn build --filter="@plunk/shared" --filter="@plunk/db"
|
||||
|
||||
- name: Generate Prisma Client
|
||||
run: yarn workspace @plunk/db db:generate
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
|
||||
- name: Run database migrations
|
||||
run: yarn workspace @plunk/db migrate:prod
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
DIRECT_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/plunk_test
|
||||
|
||||
- name: Create MinIO bucket
|
||||
run: |
|
||||
docker run --rm --network host \
|
||||
--entrypoint /bin/sh minio/mc:latest \
|
||||
-c "mc alias set local http://localhost:9000 plunk plunkminiopass && \
|
||||
mc mb local/uploads --ignore-existing"
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:run
|
||||
env:
|
||||
NODE_ENV: test
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
coverage/
|
||||
**/*.test.ts.log
|
||||
retention-days: 7
|
||||
|
||||
lint:
|
||||
name: Lint & Type Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup minimal env (for build)
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
NODE_ENV=development
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk
|
||||
DIRECT_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/plunk
|
||||
REDIS_URL=redis://localhost:6379
|
||||
JWT_SECRET=test
|
||||
|
||||
# S3 (minimal - not needed but schema may require)
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
S3_ACCESS_KEY_ID=mock
|
||||
S3_ACCESS_KEY_SECRET=mock
|
||||
S3_BUCKET=uploads
|
||||
S3_PUBLIC_URL=http://localhost:9000/uploads
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# Application URLs
|
||||
API_URI=http://localhost:8080
|
||||
DASHBOARD_URI=http://localhost:3000
|
||||
LANDING_URI=http://localhost:4000
|
||||
WIKI_URI=http://localhost:1000
|
||||
|
||||
# AWS SES (mock)
|
||||
AWS_SES_REGION=us-east-1
|
||||
AWS_SES_ACCESS_KEY_ID=mock
|
||||
AWS_SES_SECRET_ACCESS_KEY=mock
|
||||
SES_CONFIGURATION_SET=test
|
||||
SES_CONFIGURATION_SET_NO_TRACKING=test
|
||||
EOF
|
||||
|
||||
- name: Build shared packages
|
||||
run: yarn build --filter="@plunk/shared" --filter="@plunk/db"
|
||||
|
||||
- name: Run linter
|
||||
run: yarn lint
|
||||
|
||||
- name: Type check
|
||||
run: yarn build --filter="api" --filter="web" --filter="landing" --filter="wiki" || true
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Build and Push Docker image (Canary)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
driaug/plunk:canary
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Build and Push Docker image (Production)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Get version from package.json
|
||||
id: get_version
|
||||
run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
driaug/plunk:${{ env.VERSION }}
|
||||
driaug/plunk:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Docker Build and Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Turborepo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Check if this commit is a release
|
||||
id: check-release
|
||||
run: |
|
||||
git fetch --tags
|
||||
|
||||
# Check if this is a release-please commit
|
||||
if git log -1 --pretty=%B | grep -q "release-please--branches--next"; then
|
||||
echo "This is a release commit, waiting for tag..."
|
||||
# Wait up to 60 seconds for release-please to create the tag
|
||||
for i in {1..12}; do
|
||||
sleep 5
|
||||
git fetch --tags
|
||||
TAG=$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || echo "")
|
||||
if [[ -n "$TAG" ]]; then
|
||||
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||
echo "release_tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "Found release tag: $TAG"
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting for tag... ($i/12)"
|
||||
done
|
||||
echo "ERROR: Release commit but no tag found after 60s"
|
||||
exit 1
|
||||
else
|
||||
# Regular commit, check for tag immediately
|
||||
TAG=$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || echo "")
|
||||
if [[ -n "$TAG" ]]; then
|
||||
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||
echo "release_tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "This is a release: $TAG"
|
||||
else
|
||||
echo "is_release=false" >> $GITHUB_OUTPUT
|
||||
echo "This is a regular commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
docker system prune -af --volumes
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
buildkitd-flags: --debug
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Compute tags
|
||||
id: tags
|
||||
run: |
|
||||
if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then
|
||||
# This is a release: use semver tags
|
||||
TAG="${{ steps.check-release.outputs.release_tag }}"
|
||||
VERSION="${TAG#v}"
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
TAGS="${VERSION},${MAJOR}.${MINOR},${MAJOR},latest"
|
||||
echo "Building RELEASE with tags: $TAGS"
|
||||
else
|
||||
# Regular commit: use SHA tags
|
||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||
TAGS="sha-${SHORT_SHA},latest"
|
||||
echo "Building COMMIT with tags: $TAGS"
|
||||
fi
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
run: |
|
||||
IMAGE="ghcr.io/${{ github.repository }}"
|
||||
TAGS="${{ steps.tags.outputs.tags }}"
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
$(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \
|
||||
--cache-from type=gha \
|
||||
--cache-to type=gha,mode=max \
|
||||
--build-arg BUILDTIME="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
|
||||
--build-arg VERSION="${{ github.ref_name }}" \
|
||||
--build-arg REVISION="${{ github.sha }}" \
|
||||
.
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
IMAGE="ghcr.io/${{ github.repository }}"
|
||||
TAGS="${{ steps.tags.outputs.tags }}"
|
||||
echo "## Docker Image Published 🚀" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ steps.check-release.outputs.is_release }}" == "true" ]]; then
|
||||
echo "**Type:** Release (${{ steps.check-release.outputs.release_tag }})" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Type:** Commit (sha-${GITHUB_SHA:0:7})" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$TAGS" | tr ',' '\n' | sed "s|^|${IMAGE}:|" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Release Please
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- next
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Release Please
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+46
-8
@@ -1,12 +1,50 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
||||
*.log
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.tscache
|
||||
.next
|
||||
.out
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# Build Outputs
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
dist
|
||||
.source/
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.yarn/install-state.gz
|
||||
*.pem
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
robots.txt
|
||||
*sitemap*.xml
|
||||
|
||||
.contentlayer
|
||||
.content-collections
|
||||
.source
|
||||
|
||||
install-state.gz
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"quoteProps": "consistent",
|
||||
"arrowParens": "avoid",
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": false,
|
||||
"useTabs": false,
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.0.1"
|
||||
}
|
||||
Vendored
-894
File diff suppressed because one or more lines are too long
+948
File diff suppressed because one or more lines are too long
+7
-1
@@ -1,3 +1,9 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.3.0.cjs
|
||||
# Support multiple architectures - download pre-built binaries instead of compiling
|
||||
supportedArchitectures:
|
||||
os: [ "linux", "darwin" ]
|
||||
cpu: [ "x64", "arm64" ]
|
||||
libc: [ "glibc" ]
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,33 +0,0 @@
|
||||
// source.config.ts
|
||||
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
|
||||
|
||||
// lib/remark-replace-env.mjs
|
||||
import { visit } from "unist-util-visit";
|
||||
var API_URL = process.env.NEXT_PUBLIC_API_URI || "https://api.useplunk.com";
|
||||
var DASHBOARD_URL = process.env.NEXT_PUBLIC_DASHBOARD_URI || "https://app.useplunk.com";
|
||||
function remarkReplaceEnv() {
|
||||
return (tree) => {
|
||||
visit(tree, ["code", "inlineCode", "text", "link"], (node) => {
|
||||
if (node.value && typeof node.value === "string") {
|
||||
node.value = node.value.replace(/\{\{API_URL\}\}/g, API_URL).replace(/\{\{DASHBOARD_URL\}\}/g, DASHBOARD_URL);
|
||||
}
|
||||
if (node.url && typeof node.url === "string") {
|
||||
node.url = node.url.replace(/\{\{API_URL\}\}/g, API_URL).replace(/\{\{DASHBOARD_URL\}\}/g, DASHBOARD_URL);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// source.config.ts
|
||||
var docs = defineDocs({
|
||||
dir: "content/docs"
|
||||
});
|
||||
var source_config_default = defineConfig({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkReplaceEnv]
|
||||
}
|
||||
});
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,224 +0,0 @@
|
||||
body, html {
|
||||
margin:0; padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, Arial;
|
||||
font-size: 14px;
|
||||
color:#333;
|
||||
}
|
||||
.small { font-size: 12px; }
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing:border-box;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0;}
|
||||
h2 { font-size: 14px; }
|
||||
pre {
|
||||
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
}
|
||||
a { color:#0074D9; text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
.strong { font-weight: bold; }
|
||||
.space-top1 { padding: 10px 0 0 0; }
|
||||
.pad2y { padding: 20px 0; }
|
||||
.pad1y { padding: 10px 0; }
|
||||
.pad2x { padding: 0 20px; }
|
||||
.pad2 { padding: 20px; }
|
||||
.pad1 { padding: 10px; }
|
||||
.space-left2 { padding-left:55px; }
|
||||
.space-right2 { padding-right:20px; }
|
||||
.center { text-align:center; }
|
||||
.clearfix { display:block; }
|
||||
.clearfix:after {
|
||||
content:'';
|
||||
display:block;
|
||||
height:0;
|
||||
clear:both;
|
||||
visibility:hidden;
|
||||
}
|
||||
.fl { float: left; }
|
||||
@media only screen and (max-width:640px) {
|
||||
.col3 { width:100%; max-width:100%; }
|
||||
.hide-mobile { display:none!important; }
|
||||
}
|
||||
|
||||
.quiet {
|
||||
color: #7f7f7f;
|
||||
color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.quiet a { opacity: 0.7; }
|
||||
|
||||
.fraction {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
background: #E8E8E8;
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.path a:link, div.path a:visited { color: #333; }
|
||||
table.coverage {
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.coverage td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.coverage td.line-count {
|
||||
text-align: right;
|
||||
padding: 0 5px 0 20px;
|
||||
}
|
||||
table.coverage td.line-coverage {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
min-width:20px;
|
||||
}
|
||||
|
||||
table.coverage td span.cline-any {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
}
|
||||
.missing-if-branch {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #333;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.skip-if-branch {
|
||||
display: none;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
background: #ccc;
|
||||
color: white;
|
||||
}
|
||||
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||
color: inherit !important;
|
||||
}
|
||||
.coverage-summary {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||
.keyline-all { border: 1px solid #ddd; }
|
||||
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||
.coverage-summary td:last-child { border-right: none; }
|
||||
.coverage-summary th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.coverage-summary th.file { border-right: none !important; }
|
||||
.coverage-summary th.pct { }
|
||||
.coverage-summary th.pic,
|
||||
.coverage-summary th.abs,
|
||||
.coverage-summary td.pct,
|
||||
.coverage-summary td.abs { text-align: right; }
|
||||
.coverage-summary td.file { white-space: nowrap; }
|
||||
.coverage-summary td.pic { min-width: 120px !important; }
|
||||
.coverage-summary tfoot td { }
|
||||
|
||||
.coverage-summary .sorter {
|
||||
height: 10px;
|
||||
width: 7px;
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
.coverage-summary .sorted .sorter {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
.coverage-summary .sorted-desc .sorter {
|
||||
background-position: 0 -10px;
|
||||
}
|
||||
.status-line { height: 10px; }
|
||||
/* yellow */
|
||||
.cbranch-no { background: yellow !important; color: #111; }
|
||||
/* dark red */
|
||||
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||
.low .chart { border:1px solid #C21F39 }
|
||||
.highlighted,
|
||||
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||
background: #C21F39 !important;
|
||||
}
|
||||
/* medium red */
|
||||
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||
/* light red */
|
||||
.low, .cline-no { background:#FCE1E5 }
|
||||
/* light green */
|
||||
.high, .cline-yes { background:rgb(230,245,208) }
|
||||
/* medium green */
|
||||
.cstat-yes { background:rgb(161,215,106) }
|
||||
/* dark green */
|
||||
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||
.high .chart { border:1px solid rgb(77,146,33) }
|
||||
/* dark yellow (gold) */
|
||||
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||
.medium .chart { border:1px solid #f9cd0b; }
|
||||
/* light yellow */
|
||||
.medium { background: #fff4c2; }
|
||||
|
||||
.cstat-skip { background: #ddd; color: #111; }
|
||||
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||
|
||||
span.cline-neutral { background: #eaeaea; }
|
||||
|
||||
.coverage-summary td.empty {
|
||||
opacity: .5;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.cover-fill, .cover-empty {
|
||||
display:inline-block;
|
||||
height: 12px;
|
||||
}
|
||||
.chart {
|
||||
line-height: 0;
|
||||
}
|
||||
.cover-empty {
|
||||
background: white;
|
||||
}
|
||||
.cover-full {
|
||||
border-right: none !important;
|
||||
}
|
||||
pre.prettyprint {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.com { color: #999 !important; }
|
||||
.ignore-none { color: #999; font-weight: normal; }
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -48px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 48px;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var jumpToCode = (function init() {
|
||||
// Classes of code we would like to highlight in the file view
|
||||
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||
|
||||
// Elements to highlight in the file listing view
|
||||
var fileListingElements = ['td.pct.low'];
|
||||
|
||||
// We don't want to select elements that are direct descendants of another match
|
||||
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||
|
||||
// Selector that finds elements on the page to which we can jump
|
||||
var selector =
|
||||
fileListingElements.join(', ') +
|
||||
', ' +
|
||||
notSelector +
|
||||
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||
|
||||
// The NodeList of matching elements
|
||||
var missingCoverageElements = document.querySelectorAll(selector);
|
||||
|
||||
var currentIndex;
|
||||
|
||||
function toggleClass(index) {
|
||||
missingCoverageElements
|
||||
.item(currentIndex)
|
||||
.classList.remove('highlighted');
|
||||
missingCoverageElements.item(index).classList.add('highlighted');
|
||||
}
|
||||
|
||||
function makeCurrent(index) {
|
||||
toggleClass(index);
|
||||
currentIndex = index;
|
||||
missingCoverageElements.item(index).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
var nextIndex = 0;
|
||||
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||
nextIndex = missingCoverageElements.length - 1;
|
||||
} else if (missingCoverageElements.length > 1) {
|
||||
nextIndex = currentIndex - 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
var nextIndex = 0;
|
||||
|
||||
if (
|
||||
typeof currentIndex === 'number' &&
|
||||
currentIndex < missingCoverageElements.length - 1
|
||||
) {
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
makeCurrent(nextIndex);
|
||||
}
|
||||
|
||||
return function jump(event) {
|
||||
if (
|
||||
document.getElementById('fileSearch') === document.activeElement &&
|
||||
document.activeElement != null
|
||||
) {
|
||||
// if we're currently focused on the search input, we don't want to navigate
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
case 78: // n
|
||||
case 74: // j
|
||||
goToNext();
|
||||
break;
|
||||
case 66: // b
|
||||
case 75: // k
|
||||
case 80: // p
|
||||
goToPrevious();
|
||||
break;
|
||||
}
|
||||
};
|
||||
})();
|
||||
window.addEventListener('keydown', jumpToCode);
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 445 B |
@@ -1,101 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>All files</h1>
|
||||
<div class='clearfix'>
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">Unknown% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<p class="quiet">
|
||||
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||
</p>
|
||||
<template id="filterTemplate">
|
||||
<div class="quiet">
|
||||
Filter:
|
||||
<input type="search" id="fileSearch">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class='status-line medium'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage generated by
|
||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||
at 2025-11-30T17:34:40.516Z
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
prettyPrint();
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
<script src="block-navigation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 138 B |
@@ -1,210 +0,0 @@
|
||||
/* eslint-disable */
|
||||
var addSorting = (function() {
|
||||
'use strict';
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() {
|
||||
return document.querySelector('.coverage-summary');
|
||||
}
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() {
|
||||
return getTable().querySelector('thead tr');
|
||||
}
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() {
|
||||
return getTable().querySelector('tbody');
|
||||
}
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) {
|
||||
return getTableHeader().querySelectorAll('th')[n];
|
||||
}
|
||||
|
||||
function onFilterInput() {
|
||||
const searchValue = document.getElementById('fileSearch').value;
|
||||
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||
|
||||
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||
// it will be treated as a plain text search
|
||||
let searchRegex;
|
||||
try {
|
||||
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||
} catch (error) {
|
||||
searchRegex = null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
let isMatch = false;
|
||||
|
||||
if (searchRegex) {
|
||||
// If a valid regex was created, use it for matching
|
||||
isMatch = searchRegex.test(row.textContent);
|
||||
} else {
|
||||
// Otherwise, fall back to the original plain text search
|
||||
isMatch = row.textContent
|
||||
.toLowerCase()
|
||||
.includes(searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
row.style.display = isMatch ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// loads the search box
|
||||
function addSearchBox() {
|
||||
var template = document.getElementById('filterTemplate');
|
||||
var templateClone = template.content.cloneNode(true);
|
||||
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||
template.parentElement.appendChild(templateClone);
|
||||
}
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML =
|
||||
colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function(a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function(a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc
|
||||
? ' sorted-desc'
|
||||
: ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function() {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i = 0; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function() {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData();
|
||||
addSearchBox();
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
||||
Reference in New Issue
Block a user