mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
79 Commits
postmortem
...
9.2.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f86579be | ||
|
|
34e1ceec33 | ||
|
|
edba39d475 | ||
|
|
5385eb2076 | ||
|
|
40597b7647 | ||
|
|
2e5cc47522 | ||
|
|
e6c7eceb57 | ||
|
|
ef2bb2f7b6 | ||
|
|
049dc02a5f | ||
|
|
100f46ab3c | ||
|
|
e21d080e6f | ||
|
|
3740382e76 | ||
|
|
d3864e268b | ||
|
|
d3db3fd174 | ||
|
|
9f9e337350 | ||
|
|
1e84ece07e | ||
|
|
ee4981bd19 | ||
|
|
f8802eea24 | ||
|
|
5b45cab822 | ||
|
|
908f16d769 | ||
|
|
e4852e5799 | ||
|
|
06e672abdd | ||
|
|
efed184276 | ||
|
|
9bd14ec3f3 | ||
|
|
9950f1502e | ||
|
|
43432261e1 | ||
|
|
bd2b8ba1fb | ||
|
|
03742ab6f4 | ||
|
|
7324bff68b | ||
|
|
8dbfa524e5 | ||
|
|
b2ef34f45f | ||
|
|
1ec9c885f3 | ||
|
|
007973aa86 | ||
|
|
500101350f | ||
|
|
524fcae430 | ||
|
|
7ec8fc5b1c | ||
|
|
6af3daa98e | ||
|
|
1d35614cd3 | ||
|
|
91219c9a96 | ||
|
|
65ca7623d5 | ||
|
|
c569977b45 | ||
|
|
2263916a9f | ||
|
|
2cca728dfc | ||
|
|
ed687a1639 | ||
|
|
270199806c | ||
|
|
30a3c5e1b2 | ||
|
|
0c5bd31023 | ||
|
|
84a75b7af6 | ||
|
|
e25be96040 | ||
|
|
7777f7d9aa | ||
|
|
8e37df3fc0 | ||
|
|
88c9e0beb5 | ||
|
|
d751537473 | ||
|
|
60be6c00e9 | ||
|
|
91bf55dc20 | ||
|
|
d20a125742 | ||
|
|
d10bcd2edd | ||
|
|
0b32408bf2 | ||
|
|
269fbd3f24 | ||
|
|
2640ea8c10 | ||
|
|
f3180d3a83 | ||
|
|
7727fe835f | ||
|
|
00fbfbc08e | ||
|
|
44d1183066 | ||
|
|
0ccef797ab | ||
|
|
9914fb905f | ||
|
|
35ecc19ceb | ||
|
|
fa0362f739 | ||
|
|
8ea9084d9e | ||
|
|
eeb31a2250 | ||
|
|
b58c91dbab | ||
|
|
868bf4d3e1 | ||
|
|
a3fc20b393 | ||
|
|
c8dad04b5c | ||
|
|
ee7db393f8 | ||
|
|
e52da9fef2 | ||
|
|
9332df5648 | ||
|
|
120fc2ad71 | ||
|
|
1a7672748f |
163
.github/workflows/release.yml
vendored
163
.github/workflows/release.yml
vendored
@@ -22,19 +22,18 @@ jobs:
|
||||
read-version:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
outputs:
|
||||
major_minor: ${{ steps.determine.outputs.semver_base }}
|
||||
semver_base: ${{ steps.determine.outputs.semver_base }}
|
||||
major: ${{ steps.determine.outputs.major }}
|
||||
minor: ${{ steps.determine.outputs.minor }}
|
||||
patch: ${{ steps.determine.outputs.patch }}
|
||||
updated: ${{ steps.determine.outputs.updated }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Determine semver base and persist patch
|
||||
- name: Check version and verify not already released
|
||||
id: determine
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,6 +47,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version from VERSION file: $VERSION_RAW"
|
||||
|
||||
IFS='.' read -r major minor patch <<< "$VERSION_RAW"
|
||||
if [[ -z "$minor" ]]; then
|
||||
echo "VERSION must contain major and minor components" >&2
|
||||
@@ -63,50 +64,27 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
target_patch="$patch"
|
||||
updated="false"
|
||||
pr_url=""
|
||||
latest_tag="$(gh release view --repo "$REPOSITORY" --json tagName --jq '.tagName' 2>/dev/null || echo "")"
|
||||
|
||||
if [[ -n "$latest_tag" ]]; then
|
||||
latest_tag="${latest_tag#v}"
|
||||
latest_tag_core="${latest_tag%%+*}"
|
||||
latest_tag_core="${latest_tag_core%%-*}"
|
||||
IFS='.' read -r rel_major rel_minor rel_patch _ <<< "$latest_tag_core"
|
||||
rel_patch="${rel_patch:-0}"
|
||||
if [[ "$rel_major" =~ ^[0-9]+$ && "$rel_minor" =~ ^[0-9]+$ && "$rel_patch" =~ ^[0-9]+$ ]]; then
|
||||
if [[ "$rel_major" == "$major" && "$rel_minor" == "$minor" ]]; then
|
||||
target_patch=$((rel_patch + 1))
|
||||
fi
|
||||
fi
|
||||
# Check if this version is already released on GitHub
|
||||
echo "Checking if version $VERSION_RAW is already released on GitHub..."
|
||||
|
||||
if gh release view "$VERSION_RAW" --repo "$REPOSITORY" &>/dev/null; then
|
||||
echo "::error::Version $VERSION_RAW is already released on GitHub. Please update the VERSION file to a new version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Also check with 'v' prefix just in case
|
||||
if gh release view "v$VERSION_RAW" --repo "$REPOSITORY" &>/dev/null; then
|
||||
echo "::error::Version v$VERSION_RAW is already released on GitHub. Please update the VERSION file to a new version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new_version="${major}.${minor}.${target_patch}"
|
||||
if [[ "$new_version" != "$VERSION_RAW" ]]; then
|
||||
echo "$new_version" > VERSION
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add VERSION
|
||||
if ! git diff --cached --quiet; then
|
||||
branch_name="chore/bump-version-${new_version}-$(date +%s)"
|
||||
git checkout -b "$branch_name"
|
||||
git commit -m "chore: bump version to ${new_version} [skip ci]"
|
||||
git push origin "$branch_name"
|
||||
pr_title="chore: bump version to ${new_version}"
|
||||
pr_body=$'Automated change to VERSION to align release workflow with master.\n\nCreated by GitHub Actions release workflow.'
|
||||
gh pr create --repo "$REPOSITORY" --base master --head "$branch_name" --title "$pr_title" --body "$pr_body"
|
||||
pr_url="$(gh pr view "$branch_name" --repo "$REPOSITORY" --json url --jq '.url' 2>/dev/null || true)"
|
||||
updated="true"
|
||||
fi
|
||||
fi
|
||||
echo "✅ Version $VERSION_RAW is not yet released. Proceeding with release."
|
||||
|
||||
echo "semver_base=${new_version}" >> "$GITHUB_OUTPUT"
|
||||
echo "semver_base=${VERSION_RAW}" >> "$GITHUB_OUTPUT"
|
||||
echo "major=${major}" >> "$GITHUB_OUTPUT"
|
||||
echo "minor=${minor}" >> "$GITHUB_OUTPUT"
|
||||
echo "patch=${target_patch}" >> "$GITHUB_OUTPUT"
|
||||
echo "updated=${updated}" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_url=${pr_url}" >> "$GITHUB_OUTPUT"
|
||||
echo "Using version base: ${new_version}"
|
||||
echo "patch=${patch}" >> "$GITHUB_OUTPUT"
|
||||
echo "Using version: ${VERSION_RAW}"
|
||||
|
||||
|
||||
|
||||
@@ -1647,101 +1625,6 @@ jobs:
|
||||
run: bash ./Scripts/NPM/PublishAllPackages.sh
|
||||
|
||||
|
||||
llm-docker-image-deploy:
|
||||
needs: [generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/llm
|
||||
ghcr.io/oneuptime/llm
|
||||
tags: |
|
||||
type=raw,value=release,enable=true
|
||||
type=semver,value=${{needs.read-version.outputs.major_minor}},pattern={{version}},enable=true
|
||||
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
# - name: Setup Git LFS
|
||||
# run: git lfs install
|
||||
|
||||
# # Cannot do this, no space on the gitHub standard runner. We need to use the large runner which is selfhosted
|
||||
# - name: Download the Model from Hugging Face
|
||||
# run: mkdir -p ./LLM/Models && cd ./LLM/Models && git clone https://${{ secrets.HUGGING_FACE_USERNAME }}:${{ secrets.HUGGING_FACE_PASSWORD }}@huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4
|
||||
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image llm \
|
||||
--version "${{needs.read-version.outputs.major_minor}}" \
|
||||
--dockerfile ./LLM/Dockerfile \
|
||||
--context ./LLM \
|
||||
--platforms linux/amd64 \
|
||||
--git-sha "${{ github.sha }}"
|
||||
|
||||
docs-docker-image-deploy:
|
||||
needs: [generate-build-number, read-version]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -2131,7 +2014,6 @@ jobs:
|
||||
- app-docker-image-deploy
|
||||
- copilot-docker-image-deploy
|
||||
- accounts-docker-image-deploy
|
||||
- llm-docker-image-deploy
|
||||
- docs-docker-image-deploy
|
||||
- worker-docker-image-deploy
|
||||
- workflow-docker-image-deploy
|
||||
@@ -2162,7 +2044,6 @@ jobs:
|
||||
"app",
|
||||
"copilot",
|
||||
"accounts",
|
||||
"llm",
|
||||
"docs",
|
||||
"worker",
|
||||
"workflow",
|
||||
@@ -2221,7 +2102,7 @@ jobs:
|
||||
|
||||
test-e2e-release-saas:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
@@ -2309,7 +2190,7 @@ jobs:
|
||||
test-e2e-release-self-hosted:
|
||||
runs-on: ubuntu-latest
|
||||
# After all the jobs runs
|
||||
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
|
||||
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
|
||||
32
.github/workflows/reliability-copilot.yml
vendored
32
.github/workflows/reliability-copilot.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
name: "OneUptime Reliability Copilot"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
# Run every day at midnight UTC
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze Code
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
# Run Reliability Copilot in Docker Container
|
||||
- name: Run Copilot
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e ONEUPTIME_URL="https://test.oneuptime.com" \
|
||||
-e ONEUPTIME_REPOSITORY_SECRET_KEY="${{ secrets.COPILOT_ONEUPTIME_REPOSITORY_SECRET_KEY }}" \
|
||||
-e CODE_REPOSITORY_PASSWORD="${{ github.token }}" \
|
||||
-e CODE_REPOSITORY_USERNAME="simlarsen" \
|
||||
-e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
|
||||
--net=host oneuptime/copilot:test
|
||||
99
.github/workflows/test-release.yaml
vendored
99
.github/workflows/test-release.yaml
vendored
@@ -207,103 +207,6 @@ jobs:
|
||||
|
||||
|
||||
|
||||
llm-docker-image-deploy:
|
||||
needs: [read-version, generate-build-number]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oneuptime/llm
|
||||
ghcr.io/oneuptime/llm
|
||||
tags: |
|
||||
type=raw,value=test,enable=true
|
||||
type=raw,value=${{needs.read-version.outputs.major_minor}}-test,enable=true
|
||||
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
# - name: Setup Git LFS
|
||||
# run: git lfs install
|
||||
|
||||
# # Cannot do this, no space on the gitHub standard runner. We need to use the large runner which is selfhosted
|
||||
# - name: Download the Model from Hugging Face
|
||||
# run: mkdir -p ./LLM/Models && cd ./LLM/Models && git clone https://${{ secrets.HUGGING_FACE_USERNAME }}:${{ secrets.HUGGING_FACE_PASSWORD }}@huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate Dockerfile from Dockerfile.tpl
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run prerun
|
||||
|
||||
# Build and deploy nginx.
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
bash ./Scripts/GHA/build_docker_images.sh \
|
||||
--image llm \
|
||||
--version "${{needs.read-version.outputs.major_minor}}-test" \
|
||||
--dockerfile ./LLM/Dockerfile \
|
||||
--context ./LLM \
|
||||
--platforms linux/amd64 \
|
||||
--git-sha "${{ github.sha }}" \
|
||||
--extra-tags test \
|
||||
--extra-enterprise-tags enterprise-test
|
||||
|
||||
|
||||
nginx-docker-image-deploy:
|
||||
needs: [read-version, generate-build-number]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1965,7 +1868,7 @@ jobs:
|
||||
|
||||
test-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [infrastructure-agent-deploy, publish-mcp-server, llm-docker-image-deploy, publish-terraform-provider, telemetry-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
needs: [infrastructure-agent-deploy, publish-mcp-server, publish-terraform-provider, telemetry-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-router-dom": "^6.30.2",
|
||||
"use-async-effect": "^2.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
26
AdminDashboard/package-lock.json
generated
26
AdminDashboard/package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.1"
|
||||
"react-router-dom": "^6.30.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.35",
|
||||
@@ -347,9 +347,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||
"version": "1.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
||||
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -946,12 +946,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
|
||||
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0"
|
||||
"@remix-run/router": "1.23.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -961,13 +961,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||
"version": "6.30.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
|
||||
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.0",
|
||||
"react-router": "6.30.1"
|
||||
"@remix-run/router": "1.23.1",
|
||||
"react-router": "6.30.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.1"
|
||||
"react-router-dom": "^6.30.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-build": "NODE_ENV=development node esbuild.config.js",
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=alertSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text="" }}
|
||||
{{> DetailBoxField title="" text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=alertDescription }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=alertDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text="" }}
|
||||
{{> DetailBoxField title="" text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=incidentDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=alertSeverity }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=alertDescription }}
|
||||
{{> DetailBoxField title="Description: " text=alertDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=alertSeverity }}
|
||||
{{#if isPrivateNote}}
|
||||
{{> DetailBoxField title="Private Note: " text="" }}
|
||||
{{> DetailBoxField title="Private Note: " text=note }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Public Note: " text="" }}
|
||||
{{> DetailBoxField title="Public Note: " text=note }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="" text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -13,16 +13,12 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Alert Declared By: " text=declaredBy }}
|
||||
{{> DetailBoxField title="Alert Declared At: " text="" }}
|
||||
{{> DetailBoxField title="" text=declaredAt }}
|
||||
{{> DetailBoxField title="Alert Declared At: " text=declaredAt }}
|
||||
{{> DetailBoxField title="Severity: " text=alertSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text="" }}
|
||||
{{> DetailBoxField title="" text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=alertDescription }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=alertDescription }}
|
||||
{{#ifNotCond remediationNotes ""}}
|
||||
{{> DetailBoxField title="Remediation Notes: " text="" }}
|
||||
{{> DetailBoxField title="" text=remediationNotes }}
|
||||
{{> DetailBoxField title="Remediation Notes: " text=remediationNotes }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "Alert state changed to - " currentState)}}
|
||||
{{> InfoBlock info="Alert state has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> StateTransition this}}
|
||||
{{> DetailBoxField title="Alert Title:" text=alertTitle }}
|
||||
{{> DetailBoxField title="New State: " text=currentState }}
|
||||
{{> DetailBoxField title="State changed at: " text="" }}
|
||||
{{> DetailBoxField title="" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=alertSeverity }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=alertDescription }}
|
||||
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Resources Affected:" text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity:" text=alertSeverity }}
|
||||
{{> DetailBoxField title="Description:" text=alertDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
|
||||
{{> Logo this}}
|
||||
|
||||
{{> EmailTitle title="Forgot Password? We're here to help." }}
|
||||
{{> EmailTitle title="Reset Your Password" }}
|
||||
|
||||
{{> InfoBlock info="Please click on the 'Reset your password' button below which will help you reset your password and once you're done, You're good to go!"}}
|
||||
{{> InfoBlock info="We received a request to reset your password. Click the button below to create a new password for your account."}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Reset your password"}}
|
||||
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Reset Password"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
|
||||
{{> InfoBlock info=tokenVerifyUrl}}
|
||||
{{> InfoBlock info="This password reset link expires in 24 hours."}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="<strong>Note:</strong> This password reset link will expire in 24 hours. If you didn't request this reset, you can safely ignore this email."}}
|
||||
|
||||
{{> SupportBlock this }}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxField title="Description: " text=incidentDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{#if isPrivateNote}}
|
||||
{{> DetailBoxField title="Private Note: " text="" }}
|
||||
{{> DetailBoxField title="Private Note: " text=note }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Public Note: " text="" }}
|
||||
{{> DetailBoxField title="Public Note: " text=note }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="" text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -13,16 +13,12 @@
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Incident Declared By: " text=declaredBy }}
|
||||
{{> DetailBoxField title="Incident Declared At: " text="" }}
|
||||
{{> DetailBoxField title="" text=declaredAt }}
|
||||
{{> DetailBoxField title="Incident Declared At: " text=declaredAt }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Root Cause: " text="" }}
|
||||
{{> DetailBoxField title="" text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxField title="Root Cause: " text=rootCause }}
|
||||
{{> DetailBoxField title="Description: " text=incidentDescription }}
|
||||
{{#ifNotCond remediationNotes ""}}
|
||||
{{> DetailBoxField title="Remediation Notes: " text="" }}
|
||||
{{> DetailBoxField title="" text=remediationNotes }}
|
||||
{{> DetailBoxField title="Remediation Notes: " text=remediationNotes }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "Incident state changed to - " currentState)}}
|
||||
{{> InfoBlock info="Incident state has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> StateTransition this}}
|
||||
{{> DetailBoxField title="Incident Title:" text=incidentTitle }}
|
||||
{{> DetailBoxField title="New State: " text=currentState }}
|
||||
{{> DetailBoxField title="State changed at: " text="" }}
|
||||
{{> DetailBoxField title="" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Resources Affected:" text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity:" text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Description:" text=incidentDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -2,28 +2,33 @@
|
||||
|
||||
{{> Logo this}}
|
||||
|
||||
{{> EmailTitle title=(concat "You have been invited to " projectName) }}
|
||||
{{> EmailTitle title=(concat "👋 You're Invited to " projectName) }}
|
||||
|
||||
{{#ifCond isNewUser "true"}}
|
||||
{{> InfoBlock info="Please sign up to a new account to accept this invitation"}}
|
||||
{{> InfoBlock info="You've been invited to join a project on OneUptime. Create your account to get started."}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=registerLink buttonText="Sign up to a new account"}}
|
||||
{{> ButtonBlock buttonUrl=registerLink buttonText="Create Your Account"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
|
||||
{{> InfoBlock info=registerLink}}
|
||||
{{/ifCond}}
|
||||
|
||||
{{#ifCond isNewUser "false"}}
|
||||
{{> InfoBlock info="Please sign in to your account to see all your invitations and manage them."}}
|
||||
{{> InfoBlock info="You've been invited to join a project on OneUptime. Sign in to view and manage your invitations."}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=signInLink buttonText="Sign in to OneUptime"}}
|
||||
{{> ButtonBlock buttonUrl=signInLink buttonText="Sign In to OneUptime"}}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
|
||||
{{> InfoBlock info=signInLink}}
|
||||
{{/ifCond}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="If you have not signed up to OneUptime so far. You'll be redirected to the account sign up page to sign up first."}}
|
||||
{{> InfoBlock info="<em>If you don't have an account yet, you'll be redirected to create one first.</em>"}}
|
||||
|
||||
{{> SupportBlock this }}
|
||||
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
|
||||
{{#ifNotCond monitorDestination ""}}
|
||||
{{#ifCond monitorType "API"}}
|
||||
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
|
||||
{{/ifCond}}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Current Status: " text=currentStatus }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=monitorDescription }}
|
||||
{{> DetailBoxField title="Description: " text=monitorDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
|
||||
{{#ifNotCond monitorDestination ""}}
|
||||
{{#ifCond monitorType "API"}}
|
||||
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
|
||||
{{/ifCond}}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Current Status: " text=currentStatus }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=monitorDescription }}
|
||||
{{> DetailBoxField title="Description: " text=monitorDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -4,21 +4,28 @@
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Monitor: " monitorName) }}
|
||||
|
||||
{{> InfoBlock info=(concat "Monitor status changed to - " currentStatus)}}
|
||||
{{> InfoBlock info="Monitor status has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
|
||||
{{> DetailBoxField title="New Status: " text=currentStatus }}
|
||||
{{#ifNotCond rootCause ""}}
|
||||
{{> DetailBoxField title="Root Cause: " text="" }}
|
||||
{{> DetailBoxField title="" text=rootCause }}
|
||||
{{> StatusTransition this}}
|
||||
{{#ifNotCond previousStatusDurationText ""}}
|
||||
{{> DetailBoxField title="Duration in Previous Status:" text=previousStatusDurationText }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Status changed at: " text="" }}
|
||||
{{> DetailBoxField title="" text=statusChangedAt }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=monitorDescription }}
|
||||
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
|
||||
{{#ifNotCond monitorDestination ""}}
|
||||
{{#ifCond monitorType "API"}}
|
||||
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
|
||||
{{/ifCond}}
|
||||
{{/ifNotCond}}
|
||||
{{#ifNotCond rootCause ""}}
|
||||
{{> DetailBoxField title="Root Cause:" text=rootCause }}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Status changed at:" text=statusChangedAt }}
|
||||
{{> DetailBoxField title="Description:" text=monitorDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
|
||||
{{> DetailBoxField title="Monitor Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=monitorDescription }}
|
||||
{{#ifNotCond monitorDestination ""}}
|
||||
{{#ifCond monitorType "API"}}
|
||||
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
|
||||
{{/ifCond}}
|
||||
{{/ifNotCond}}
|
||||
{{> DetailBoxField title="Monitor Description: " text=monitorDescription }}
|
||||
{{> DetailBoxField title="Probe Status: " text=currentStatus }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Primary Action Button -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
@@ -8,26 +9,26 @@
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--body"
|
||||
style="border: 0; margin: 0; padding: 0; color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
|
||||
style="border: 0; margin: 0; padding: 0; color: #1a1a2e; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
|
||||
|
||||
<!-- Button & Modifier: fullWidth -->
|
||||
<!-- Button & Modifier: fullWidth with gradient -->
|
||||
<table class="st-Button st-Button--fullWidth" border="0" cellpadding="0" cellspacing="0"
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" class="st-Button-area" height="38" valign="middle"
|
||||
style="border: 0; margin: 0; padding: 0; background-color: #000000; border-radius: 5px; text-align: center;">
|
||||
<td align="center" class="st-Button-area" height="48" valign="middle"
|
||||
style="border: 0; margin: 0; padding: 0; background: linear-gradient(135deg, #1a1a2e 0%, #000000 100%); border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);">
|
||||
<a class="st-Button-link"
|
||||
style="border: 0; margin: 0; padding: 0; color: #ffffff; display: block; height: 38px; text-align: center; text-decoration: none;"
|
||||
style="border: 0; margin: 0; padding: 0; color: #ffffff; display: block; height: 48px; text-align: center; text-decoration: none;"
|
||||
href={{buttonUrl}}>
|
||||
<span class="st-Button-internal"
|
||||
style="border: 0; margin: 0; padding: 0; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; font-weight: bold; height: 38px; line-height: 38px; mso-line-height-rule: exactly; text-decoration: none; vertical-align: middle; white-space: nowrap; width: 100%;">{{buttonText}}</span>
|
||||
style="border: 0; margin: 0; padding: 0; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; font-weight: 600; height: 48px; line-height: 48px; mso-line-height-rule: exactly; text-decoration: none; vertical-align: middle; white-space: nowrap; width: 100%; letter-spacing: 0.3px;">{{buttonText}}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Button & Modifier: fullWidth -->
|
||||
<!-- /Button & Modifier: fullWidth with gradient -->
|
||||
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
@@ -37,10 +38,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
<!-- /Primary Action Button -->
|
||||
@@ -19,7 +19,7 @@
|
||||
<a style="border: 0; margin: 0; padding: 0; text-decoration: none;" href={{homeURL}}>
|
||||
|
||||
<img alt="OneUptime" height="70" border="0"
|
||||
style="height:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal;"
|
||||
style="height:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal; border-radius: 10%;"
|
||||
src="{{logoUrl}}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
<!-- End Detail Card Field Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -10,4 +10,5 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
<!-- /Detail Card Container -->
|
||||
@@ -1,7 +1,9 @@
|
||||
<p style="Margin:0;font-size:16px;font-family:'inter','helvetica neue',helvetica,arial,sans-serif;line-height:30px;color:#424761">
|
||||
<!-- Detail Card Field -->
|
||||
<div class="st-DetailCard-field" style="padding: 10px 0; border-bottom: 1px solid #e2e8f0;">
|
||||
{{#if title}}
|
||||
<strong>{{{title}}} </strong>{{{text}}} </span>
|
||||
{{else}}
|
||||
{{{text}}}
|
||||
<p class="st-DetailCard-label" style="Margin:0 0 4px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">{{{title}}}</p>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#if text}}
|
||||
<p class="st-DetailCard-value" style="Margin:0;font-size:15px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:24px;color:#1e293b;font-weight:500;">{{{text}}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,19 +1,20 @@
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="500" style="min-width: 500px;margin: 10px 50px;">
|
||||
<!-- Detail Card Container -->
|
||||
<table class="st-Copy st-Width st-Width--mobile st-DetailCard" border="0" cellpadding="0" cellspacing="0"
|
||||
width="472" style="min-width: 472px; margin: 16px 64px; border-radius: 12px; overflow: hidden;">
|
||||
<tbody>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td width="720" align="center" valign="top" style="padding:0;Margin:0"></td>
|
||||
<td width="100%" align="center" valign="top" style="padding:0;Margin:0"></td>
|
||||
</tr>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td align="left"
|
||||
style="padding:0;Margin:0;padding-top:0px;padding-left:40px;padding-right:40px;padding-bottom:30px;background-color:#f7f8fa;border-radius: 5px">
|
||||
style="padding:0;Margin:0;padding-top:0px;padding-left:0;padding-right:0;padding-bottom:0;background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);border-radius: 12px; border: 1px solid #e2e8f0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);">
|
||||
<table cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse:collapse;border-spacing:0px">
|
||||
<tbody>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td width="720" align="center" valign="top" style="padding:0;Margin:0">
|
||||
<td width="100%" align="center" valign="top" style="padding:0;Margin:0">
|
||||
<table cellpadding="0" cellspacing="0" width="100%" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px">
|
||||
<tbody>
|
||||
<tr style="border-collapse:collapse">
|
||||
<td align="left" style="padding:0;Margin:0;padding-top:30px">
|
||||
<td align="left" style="padding:24px 28px 0 28px;Margin:0;">
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Email Title Block -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
@@ -7,10 +8,10 @@
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--body"
|
||||
style="color: #000000 !important; border:0;margin:0;padding:0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Ubuntu,sans-serif;font-size:16px;line-height:24px">
|
||||
<td class="st-Font st-Font--title"
|
||||
style="color: #1a1a2e !important; border:0;margin:0;padding:0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;font-size:24px;line-height:32px;font-weight:700;">
|
||||
|
||||
<h3>{{title}}</h3>
|
||||
<h2 style="margin: 0; padding: 0; font-size: 24px; line-height: 32px; font-weight: 700; color: #1a1a2e;">{{title}}</h2>
|
||||
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
@@ -20,10 +21,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
<!-- /Email Title Block -->
|
||||
@@ -7,7 +7,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--emailEnd" height="64"
|
||||
<td class="st-Spacer st-Spacer--emailEnd" height="48"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"> </div>
|
||||
</td>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!-- Footer Block -->
|
||||
<table class="st-Footer st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--divider" colspan="3" height="20"
|
||||
<td class="st-Spacer st-Spacer--divider" colspan="3" height="24"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
@@ -13,7 +14,7 @@
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td bgcolor="#fdfdfd" colspan="2" height="1"
|
||||
<td bgcolor="#e5e7eb" colspan="2" height="1"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
@@ -24,7 +25,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--divider" colspan="3" height="31"
|
||||
<td class="st-Spacer st-Spacer--divider" colspan="3" height="24"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
@@ -36,10 +37,10 @@
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--caption"
|
||||
style="border: 0; margin: 0;padding: 0; color: #8898aa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; line-height: 16px;">
|
||||
style="border: 0; margin: 0;padding: 0; color: #6b7280; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 13px; line-height: 18px; text-align: center;">
|
||||
<span class="st-Delink st-Delink--footer"
|
||||
style="border: 0; margin: 0; padding: 0; color: #8898aa; text-decoration: none;">
|
||||
© {{year}} OneUptime
|
||||
style="border: 0; margin: 0; padding: 0; color: #6b7280; text-decoration: none;">
|
||||
© {{year}} OneUptime · Powered by <a href="https://oneuptime.com" style="color: #1a1a2e; text-decoration: none; font-weight: 500;">OneUptime</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
@@ -49,10 +50,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--emailEnd" colspan="3" height="64"
|
||||
<td class="st-Spacer st-Spacer--emailEnd" colspan="3" height="48"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
<!-- /Footer Block -->
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
<!-- Info Block -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--body"
|
||||
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
|
||||
style="border: 0; margin: 0; padding: 0; color: #374151 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; line-height: 26px;">
|
||||
{{{info}}}
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
@@ -19,12 +19,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<!-- /Info Block -->
|
||||
@@ -18,7 +18,7 @@
|
||||
<a style="border: 0; margin: 0; padding: 0; text-decoration: none;" href={{homeURL}}>
|
||||
|
||||
<img height="70" width="70" alt="OneUptime" border="0"
|
||||
style="height:70px; width:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal;"
|
||||
style="height:70px; width:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal; border-radius: 10%;"
|
||||
src="https://res.cloudinary.com/deityhub/image/upload/v1637736803/1png.png">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,21 +5,33 @@
|
||||
{{> Header}}
|
||||
|
||||
|
||||
<body class="st-Email" bgcolor="f7f7f7"
|
||||
<body class="st-Email" bgcolor="f3f4f6"
|
||||
style="border: 0; margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; min-width: 100%; width: 100%;"
|
||||
override="fix">
|
||||
|
||||
<!-- Background -->
|
||||
<table class="st-Background" bgcolor="f7f7f7" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
<table class="st-Background" bgcolor="f3f4f6" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border: 0; margin: 0; padding: 0;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border: 0; margin: 0; padding: 0;">
|
||||
|
||||
<!-- Top Spacer -->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td height="32" style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px;">
|
||||
<div> </div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Top Spacer -->
|
||||
|
||||
<!-- Wrapper -->
|
||||
<table class="st-Wrapper" align="center" bgcolor="ffffff" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600"
|
||||
style="border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; margin: 0 auto; min-width: 600px;">
|
||||
style="border-radius: 12px; margin: 0 auto; min-width: 600px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); border-top: 4px solid #1a1a2e;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border: 0; margin: 0; padding: 0;">
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- State Transition Block - Inline version for DetailBox -->
|
||||
<div class="st-DetailCard-field" style="padding: 16px 0; border-bottom: 1px solid #e2e8f0;">
|
||||
<p class="st-DetailCard-label" style="Margin:0 0 12px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">STATE CHANGE</p>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#if previousState}}
|
||||
<!-- Previous State Badge -->
|
||||
<td align="center" valign="middle" style="padding: 0;">
|
||||
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{previousStateColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
|
||||
<span style="width: 10px; height: 10px; border-radius: 4px; background-color: {{previousStateColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
|
||||
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{previousState}}</span>
|
||||
</span>
|
||||
</td>
|
||||
<!-- Arrow -->
|
||||
<td align="center" valign="middle" style="padding: 0 12px;">
|
||||
<span style="font-size: 20px; color: #94a3b8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">→</span>
|
||||
</td>
|
||||
{{/if}}
|
||||
<!-- Current State Badge -->
|
||||
<td align="center" valign="middle" style="padding: 0;">
|
||||
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{currentStateColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
|
||||
<span style="width: 10px; height: 10px; border-radius: 4px; background-color: {{currentStateColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
|
||||
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{currentState}}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /State Transition Block -->
|
||||
@@ -0,0 +1,58 @@
|
||||
<!-- Status Badge - Used for incident states, severity levels, etc. -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td style="border: 0; margin: 0; padding: 0;">
|
||||
|
||||
{{#ifCond badgeType "critical"}}
|
||||
<span class="st-Badge st-Badge--critical" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #fef2f2; color: #dc2626; border: 1px solid #fecaca; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
|
||||
{{badgeText}}
|
||||
</span>
|
||||
{{/ifCond}}
|
||||
|
||||
{{#ifCond badgeType "warning"}}
|
||||
<span class="st-Badge st-Badge--warning" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #fffbeb; color: #d97706; border: 1px solid #fde68a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
|
||||
{{badgeText}}
|
||||
</span>
|
||||
{{/ifCond}}
|
||||
|
||||
{{#ifCond badgeType "success"}}
|
||||
<span class="st-Badge st-Badge--success" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
|
||||
{{badgeText}}
|
||||
</span>
|
||||
{{/ifCond}}
|
||||
|
||||
{{#ifCond badgeType "info"}}
|
||||
<span class="st-Badge st-Badge--info" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
|
||||
{{badgeText}}
|
||||
</span>
|
||||
{{/ifCond}}
|
||||
|
||||
{{#ifCond badgeType "neutral"}}
|
||||
<span class="st-Badge st-Badge--neutral" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #f3f4f6; color: #4b5563; border: 1px solid #d1d5db; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
|
||||
{{badgeText}}
|
||||
</span>
|
||||
{{/ifCond}}
|
||||
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Status Badge -->
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- Status Transition Block - Inline version for DetailBox -->
|
||||
<div class="st-DetailCard-field" style="padding: 16px 0; border-bottom: 1px solid #e2e8f0;">
|
||||
<p class="st-DetailCard-label" style="Margin:0 0 12px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">STATUS CHANGE</p>
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
|
||||
<tbody>
|
||||
<tr>
|
||||
{{#if previousStatus}}
|
||||
<!-- Previous Status Badge -->
|
||||
<td align="center" valign="middle" style="padding: 0;">
|
||||
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{previousStatusColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
|
||||
<span style="width: 10px; height: 10px; border-radius: 9999px; background-color: {{previousStatusColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
|
||||
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{previousStatus}}</span>
|
||||
</span>
|
||||
</td>
|
||||
<!-- Arrow -->
|
||||
<td align="center" valign="middle" style="padding: 0 12px;">
|
||||
<span style="font-size: 20px; color: #94a3b8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">→</span>
|
||||
</td>
|
||||
{{/if}}
|
||||
<!-- Current Status Badge -->
|
||||
<td align="center" valign="middle" style="padding: 0;">
|
||||
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{currentStatusColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
|
||||
<span style="width: 10px; height: 10px; border-radius: 9999px; background-color: {{currentStatusColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
|
||||
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{currentStatus}}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /Status Transition Block -->
|
||||
@@ -52,7 +52,7 @@
|
||||
**/
|
||||
|
||||
span.st-Delink a {
|
||||
color: #000000 !important;
|
||||
color: #1a1a2e !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
/** Modifier: title */
|
||||
span.st-Delink.st-Delink--title a {
|
||||
color: #000000 !important;
|
||||
color: #1a1a2e !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
@@ -74,14 +74,111 @@
|
||||
|
||||
/** Modifier: footer */
|
||||
span.st-Delink.st-Delink--footer a {
|
||||
color: #8898aa !important;
|
||||
color: #6b7280 !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
|
||||
.ii a[href] {
|
||||
color: #000000;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
/**
|
||||
* # Links
|
||||
* - Styled links with modern colors
|
||||
**/
|
||||
|
||||
a.st-Link {
|
||||
color: #1a1a2e !important;
|
||||
text-decoration: none !important;
|
||||
transition: color 0.2s ease !important;
|
||||
}
|
||||
|
||||
a.st-Link:hover {
|
||||
color: #000000 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* # Status Badges
|
||||
* - Colored badges for incident/alert states
|
||||
**/
|
||||
|
||||
.st-Badge {
|
||||
display: inline-block !important;
|
||||
padding: 4px 12px !important;
|
||||
border-radius: 9999px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
}
|
||||
|
||||
.st-Badge--critical {
|
||||
background-color: #fef2f2 !important;
|
||||
color: #dc2626 !important;
|
||||
border: 1px solid #fecaca !important;
|
||||
}
|
||||
|
||||
.st-Badge--warning {
|
||||
background-color: #fffbeb !important;
|
||||
color: #d97706 !important;
|
||||
border: 1px solid #fde68a !important;
|
||||
}
|
||||
|
||||
.st-Badge--success {
|
||||
background-color: #f0fdf4 !important;
|
||||
color: #16a34a !important;
|
||||
border: 1px solid #bbf7d0 !important;
|
||||
}
|
||||
|
||||
.st-Badge--info {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: #1a1a2e !important;
|
||||
border: 1px solid #d1d5db !important;
|
||||
}
|
||||
|
||||
.st-Badge--neutral {
|
||||
background-color: #f3f4f6 !important;
|
||||
color: #4b5563 !important;
|
||||
border: 1px solid #d1d5db !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* # Detail Cards
|
||||
* - Modern card styling for detail boxes
|
||||
**/
|
||||
|
||||
.st-DetailCard {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.st-DetailCard-field {
|
||||
padding: 12px 0 !important;
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.st-DetailCard-field:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.st-DetailCard-label {
|
||||
color: #64748b !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.st-DetailCard-value {
|
||||
color: #1e293b !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +204,7 @@
|
||||
|
||||
/** Modifier: gutter */
|
||||
body[override] td.st-Spacer.st-Spacer--gutter {
|
||||
width: 32px !important;
|
||||
width: 24px !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -134,8 +231,8 @@
|
||||
body[override] td.st-Font.st-Font--title,
|
||||
body[override] td.st-Font.st-Font--title span,
|
||||
body[override] td.st-Font.st-Font--title a {
|
||||
font-size: 28px !important;
|
||||
line-height: 36px !important;
|
||||
font-size: 26px !important;
|
||||
line-height: 34px !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -144,8 +241,8 @@
|
||||
body[override] td.st-Font.st-Font--header,
|
||||
body[override] td.st-Font.st-Font--header span,
|
||||
body[override] td.st-Font.st-Font--header a {
|
||||
font-size: 24px !important;
|
||||
line-height: 32px !important;
|
||||
font-size: 22px !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -154,8 +251,8 @@
|
||||
body[override] td.st-Font.st-Font--body,
|
||||
body[override] td.st-Font.st-Font--body span,
|
||||
body[override] td.st-Font.st-Font--body a {
|
||||
font-size: 18px !important;
|
||||
line-height: 28px !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 26px !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -164,8 +261,8 @@
|
||||
body[override] td.st-Font.st-Font--caption,
|
||||
body[override] td.st-Font.st-Font--caption span,
|
||||
body[override] td.st-Font.st-Font--caption a {
|
||||
font-size: 14px !important;
|
||||
line-height: 20px !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 19px !important;
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -185,7 +282,7 @@
|
||||
|
||||
body[override] table.st-Divider td.st-Spacer.st-Spacer--gutter,
|
||||
body[override] tr.st-Divider td.st-Spacer.st-Spacer--gutter {
|
||||
background-color: #000000;
|
||||
background-color: #1a1a2e;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,10 +309,23 @@
|
||||
body[override] table.st-Button td.st-Button-area,
|
||||
body[override] table.st-Button td.st-Button-area a.st-Button-link,
|
||||
body[override] table.st-Button td.st-Button-area span.st-Button-internal {
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
font-size: 18px !important;
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
font-size: 16px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* # Detail Cards - Mobile
|
||||
**/
|
||||
|
||||
body[override] .st-DetailCard {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
body[override] .st-Badge {
|
||||
font-size: 11px !important;
|
||||
padding: 3px 10px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
{{> InfoBlock info="If you need help. Please do not hesitate to contact OneUptime support team at support@oneuptime.com."}}
|
||||
<!-- Support Block -->
|
||||
{{> InfoBlock info="Need help? Contact our support team at <a href='mailto:support@oneuptime.com' style='color: #1a1a2e; text-decoration: none; font-weight: 500;'>support@oneuptime.com</a>"}}
|
||||
{{> Thanks }}
|
||||
<!-- /Support Block -->
|
||||
@@ -1,2 +1,3 @@
|
||||
{{> InfoBlock info="Thanks, have a great day."}}
|
||||
{{> InfoBlock info="OneUptime Team."}}
|
||||
<!-- Thanks Block -->
|
||||
{{> InfoBlock info="<span style='color: #374151;'>Best regards,</span><br><strong style='color: #1a1a2e;'>The OneUptime Team</strong>"}}
|
||||
<!-- /Thanks Block -->
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
<!-- Title Block -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
@@ -9,8 +9,8 @@ width="600" style="min-width: 600px;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--body"
|
||||
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
|
||||
<strong>{{{title}}}</strong>
|
||||
style="border: 0; margin: 0; padding: 0; color: #1a1a2e !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; line-height: 26px;">
|
||||
<strong style="color: #1a1a2e; font-weight: 600;">{{{title}}}</strong>
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
|
||||
@@ -19,11 +19,11 @@ width="600" style="min-width: 600px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- /Title Block -->
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Unsubscribe Block -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
@@ -7,10 +8,10 @@
|
||||
width="64">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
<td class="st-Font st-Font--body"
|
||||
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; line-height: 20px;">
|
||||
<td class="st-Font st-Font--caption"
|
||||
style="border: 0; margin: 0; padding: 0; color: #6b7280 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 13px; line-height: 20px;">
|
||||
|
||||
If you wish to change preferences or unsubscribe, please click <a href="{{unsubscribeUrl}}">here.</a>
|
||||
<span style="color: #6b7280;">Want to change your notification preferences?</span> <a href="{{unsubscribeUrl}}" style="color: #1a1a2e; text-decoration: none; font-weight: 500;">Manage subscription</a>
|
||||
|
||||
</td>
|
||||
<td class="st-Spacer st-Spacer--gutter"
|
||||
@@ -20,10 +21,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
|
||||
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
|
||||
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
<!-- /Unsubscribe Block -->
|
||||
@@ -1,3 +1,12 @@
|
||||
<div style="margin-top: 10px; margin-bottom: 10px">
|
||||
|
||||
</div>
|
||||
<!-- Vertical Spacer -->
|
||||
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
|
||||
width="600" style="min-width: 600px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td height="20" style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
|
||||
<div class="st-Spacer st-Spacer--filler"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- /Vertical Spacer -->
|
||||
@@ -10,8 +10,7 @@
|
||||
{{> DetailBoxField title="Probe Name:" text=probeName }}
|
||||
{{> DetailBoxField title="Probe Description:" text=probeDescription }}
|
||||
{{> DetailBoxField title="Probe Status:" text=probeStatus }}
|
||||
{{> DetailBoxField title="Status Since:" text="" }}
|
||||
{{> DetailBoxField title="" text=lastAlive }}
|
||||
{{> DetailBoxField title="Status Since:" text=lastAlive }}
|
||||
{{> DetailBoxField title="Project Name: " text=projectName }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxField title="Description: " text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{#if isPrivateNote}}
|
||||
{{> DetailBoxField title="Private Note: " text="" }}
|
||||
{{> DetailBoxField title="Private Note: " text=note }}
|
||||
{{else}}
|
||||
{{> DetailBoxField title="Public Note: " text="" }}
|
||||
{{> DetailBoxField title="Public Note: " text=note }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="" text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
|
||||
{{> DetailBoxField title="Current State: " text=currentState }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxField title="Description: " text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "Scheduled Maintenance status changed to - " currentState)}}
|
||||
{{> InfoBlock info="Scheduled Maintenance state has changed"}}
|
||||
|
||||
{{> InfoBlock info="Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> StateTransition this}}
|
||||
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
|
||||
{{> DetailBoxField title="New State: " text=currentState }}
|
||||
{{> DetailBoxField title="State changed at: " text="" }}
|
||||
{{> DetailBoxField title="" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
|
||||
{{> DetailBoxField title="Description:" text=scheduledMaintenanceDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> Logo this}}
|
||||
{{> EmailTitle title="Welcome to OneUptime. Please verify your email." }}
|
||||
{{> InfoBlock info="Thank you for signing up. Next step would be to verify your email account. Can you please click on 'Verify Email' button which will help us to get your email verified. "}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Verify Email"}}
|
||||
{{> EmailTitle title="🎉 Welcome to OneUptime!" }}
|
||||
|
||||
{{> InfoBlock info="You can also copy and paste this link:"}}
|
||||
{{> InfoBlock info="Thank you for signing up! To get started, please verify your email address by clicking the button below."}}
|
||||
|
||||
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Verify Email Address"}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
|
||||
{{> InfoBlock info=tokenVerifyUrl}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> SupportBlock this }}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Status Page Name:" text=statusPageName }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=statusPageDescription }}
|
||||
{{> DetailBoxField title="Description: " text=statusPageDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
{{> InfoBlock info=(concat "A new announcement has been created for : " statusPageName) }}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=announcementTitle text="" }}
|
||||
{{> DetailBoxField title="" text=announcementDescription }}
|
||||
{{> DetailBoxField title=announcementTitle text=announcementDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title="Status Page Name:" text=statusPageName }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=statusPageDescription }}
|
||||
{{> DetailBoxField title="Description: " text=statusPageDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
|
||||
{{#ifCond hasResources "true"}}
|
||||
{{> InfoBlock info="Here is the more detailed report:"}}
|
||||
<table style="min-width: 500px;margin: 10px 50px; fotn-size: 15px; margin-top: 10px; margin-bottom: 20px">
|
||||
<tr style="background-color: #424761; color: white; padding: 1px">
|
||||
<th>Resource Name</th>
|
||||
<th>Uptime %</th>
|
||||
<th>Downtime</th>
|
||||
<th>Incidents</th>
|
||||
<table style="width: calc(100% - 64px); margin: 18px 32px; border-collapse: collapse; font-size: 14px; font-family: 'Helvetica Neue', Arial, sans-serif; color: #1f2733;">
|
||||
<tr>
|
||||
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: left; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040; border-top-left-radius: 10px;">Resource Name</th>
|
||||
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040;">Uptime %</th>
|
||||
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040;">Downtime</th>
|
||||
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040; border-top-right-radius: 10px;">Incidents</th>
|
||||
</tr>
|
||||
{{#each report.resources}}
|
||||
<tr style="padding: 1px;">
|
||||
<td>{{this.resourceName}}</td>
|
||||
<td style="text-align: right">{{this.uptimePercentAsString}}</td>
|
||||
<td style="text-align: right">{{this.downtimeInHoursAndMinutes}}</td>
|
||||
<td style="text-align: right">{{this.totalIncidentCount}}</td>
|
||||
<tr style="background-color: {{#if @odd}}#f0f3f9{{else}}#ffffff{{/if}};">
|
||||
<td style="padding: 12px 12px; border-bottom: 1px solid #e5e8f0;">{{this.resourceName}}</td>
|
||||
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0; font-weight: 600;">{{this.uptimePercentAsString}}</td>
|
||||
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0;">{{this.downtimeInHoursAndMinutes}}</td>
|
||||
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0; font-weight: 600;">{{this.totalIncidentCount}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> CustomLogo this}}
|
||||
{{> EmailTitle title=(concat "Announcement: " announcementTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new announcement has been created for : " statusPageName) }}
|
||||
{{> EmailTitle title=(concat "📢 Announcement: " announcementTitle) }}
|
||||
|
||||
{{> InfoBlock info=(concat "A new announcement has been posted for " statusPageName ".") }}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=announcementTitle text="" }}
|
||||
{{> DetailBoxField title="" text=announcementDescription }}
|
||||
{{> DetailBoxField title="Announcement" text=announcementTitle }}
|
||||
{{#if announcementDescription}}
|
||||
{{> DetailBoxField title="Details" text=announcementDescription }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Announcement"}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -1,35 +1,34 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
|
||||
{{> CustomLogo this}}
|
||||
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new incident has been created. Here are the details: "}}
|
||||
{{> EmailTitle title=(concat "New Incident: " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="A new incident has been reported that may affect the services you're subscribed to."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=incidentTitle text="" }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=incidentDescription }}
|
||||
{{> DetailBoxField title="Incident" text=incidentTitle }}
|
||||
{{> DetailBoxField title="Severity" text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{#if incidentDescription}}
|
||||
{{> DetailBoxField title="Description" text=incidentDescription }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -6,11 +6,10 @@
|
||||
{{> InfoBlock info="A new note has been added to the incident. Here are the details: "}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=incidentTitle text="" }}
|
||||
{{> DetailBoxField title="Incident Title: " text=incidentTitle }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Note: " text="" }}
|
||||
{{> DetailBoxField title="" text=note }}
|
||||
{{> DetailBoxField title="Note: " text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
|
||||
{{> CustomLogo this}}
|
||||
|
||||
{{> EmailTitle title=(concat "Postmortem Published: " incidentTitle) }}
|
||||
|
||||
{{> InfoBlock info="A postmortem has been published for an incident. Here are the details: "}}
|
||||
|
||||
{{> InfoBlock info="A postmortem report has been published for an incident that affected services you're subscribed to."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=incidentTitle text="" }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Postmortem: " text="" }}
|
||||
{{> DetailBoxField title="" text=postmortemNote }}
|
||||
{{> DetailBoxField title="Incident" text=incidentTitle }}
|
||||
{{> DetailBoxField title="Severity" text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{#if postmortemNote}}
|
||||
{{> DetailBoxField title="Postmortem Summary" text=postmortemNote }}
|
||||
{{/if}}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="Read Full Postmortem"}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -1,26 +1,32 @@
|
||||
{{> Start this}}
|
||||
|
||||
{{> CustomLogo this}}
|
||||
|
||||
{{> EmailTitle title=emailTitle }}
|
||||
|
||||
{{> InfoBlock info="Incident state has changed. Here are the details: "}}
|
||||
{{> InfoBlock info="The status of an incident affecting services you're subscribed to has been updated."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=incidentTitle text="" }}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
|
||||
{{> DetailBoxField title="State: " text=incidentState }}
|
||||
{{> DetailBoxField title="Incident" text=incidentTitle }}
|
||||
{{> DetailBoxField title="Current State" text=incidentState }}
|
||||
{{> DetailBoxField title="Severity" text=incidentSeverity }}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -1,36 +1,37 @@
|
||||
{{> Start this}}
|
||||
|
||||
|
||||
|
||||
{{> CustomLogo this}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance Event: " eventTitle) }}
|
||||
|
||||
{{> InfoBlock info="Here are more details for this scheduled event: "}}
|
||||
{{> EmailTitle title=(concat "Scheduled Maintenance: " eventTitle) }}
|
||||
|
||||
{{> InfoBlock info="A scheduled maintenance event has been announced for services you're subscribed to."}}
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=eventTitle text="" }}
|
||||
{{> DetailBoxField title="Event Status: " text="Scheduled" }}
|
||||
{{> DetailBoxField title="Maintenance Event" text=eventTitle }}
|
||||
{{> DetailBoxField title="Status" text="Scheduled" }}
|
||||
{{> DetailBoxField title="Scheduled Time" text=scheduledAt }}
|
||||
{{#if resourcesAffected}}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
|
||||
{{/if}}
|
||||
{{#if eventDescription}}
|
||||
{{> DetailBoxField title="Description" text=eventDescription }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="Event Scheduled At: " text="" }}
|
||||
{{> DetailBoxField title="" text=scheduledAt }}
|
||||
{{> DetailBoxField title="Event Description: " text="" }}
|
||||
{{> DetailBoxField title="" text=eventDescription }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
|
||||
|
||||
|
||||
{{#if detailsUrl}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
|
||||
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Maintenance Details"}}
|
||||
{{else}}
|
||||
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
|
||||
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
|
||||
{{/if}}
|
||||
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{#if subscriberEmailNotificationFooterText}}
|
||||
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
|
||||
{{/if}}
|
||||
|
||||
{{> UnsubscribeBlock this}}
|
||||
{{> VerticalSpace this}}
|
||||
|
||||
{{> Footer this}}
|
||||
|
||||
{{> End this}}
|
||||
@@ -9,13 +9,12 @@
|
||||
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=eventTitle text="" }}
|
||||
{{> DetailBoxField title="Event Title: " text=eventTitle }}
|
||||
{{> DetailBoxField title="Event Description: " text=eventDescription }}
|
||||
{{#if resourcesAffected}}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
{{/if}}
|
||||
{{> DetailBoxField title="Note: " text="" }}
|
||||
{{> DetailBoxField title="" text=note }}
|
||||
{{> DetailBoxField title="Note: " text=note }}
|
||||
{{> DetailBoxEnd this }}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
|
||||
{{> DetailBoxStart this }}
|
||||
{{> DetailBoxField title=eventTitle text="" }}
|
||||
{{> DetailBoxField title="Event Title: " text=eventTitle }}
|
||||
{{> DetailBoxField title="Event State: " text=eventState }}
|
||||
{{#if resourcesAffected}}
|
||||
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
|
||||
|
||||
25
App/package-lock.json
generated
25
App/package-lock.json
generated
@@ -1788,7 +1788,8 @@
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
@@ -2126,6 +2127,7 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
@@ -3596,21 +3598,23 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -4329,7 +4333,8 @@
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.3.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1764850876394 implements MigrationInterface {
|
||||
public name = "MigrationName1764850876394";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" ADD "postmortemPostedAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Incident" DROP COLUMN "postmortemPostedAt"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,7 @@ import { MigrationName1764324618043 } from "./1764324618043-MigrationName";
|
||||
import { MigrationName1764762146063 } from "./1764762146063-MigrationName";
|
||||
import { MigrationName1764767371788 } from "./1764767371788-MigrationName";
|
||||
import { MigrationName1764789433216 } from "./1764789433216-MigrationName";
|
||||
import { MigrationName1764850876394 } from "./1764850876394-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -384,4 +385,5 @@ export default [
|
||||
MigrationName1764762146063,
|
||||
MigrationName1764767371788,
|
||||
MigrationName1764789433216,
|
||||
MigrationName1764850876394,
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClusterKey } from "../EnvironmentConfig";
|
||||
import Dictionary from "../../Types/Dictionary";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import { Queue as BullQueue, Job, JobsOptions } from "bullmq";
|
||||
import { Queue as BullQueue, Job, JobsOptions, RepeatableJob } from "bullmq";
|
||||
import { ExpressAdapter } from "@bull-board/express";
|
||||
import { createBullBoard } from "@bull-board/api";
|
||||
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
|
||||
@@ -196,14 +196,29 @@ export default class Queue {
|
||||
jobId: sanitizedJobId,
|
||||
};
|
||||
|
||||
const queue: BullQueue = this.getQueue(queueName);
|
||||
|
||||
if (options && options.scheduleAt) {
|
||||
optionsObject.repeat = {
|
||||
pattern: options.scheduleAt,
|
||||
// keep repeatable job keyed by jobId so multiple workers do not register duplicates
|
||||
jobId: sanitizedJobId,
|
||||
};
|
||||
|
||||
const repeatableJobs: RepeatableJob[] = await queue.getRepeatableJobs();
|
||||
|
||||
for (const repeatableJob of repeatableJobs) {
|
||||
const isSameJob: boolean =
|
||||
repeatableJob.name === jobName &&
|
||||
repeatableJob.pattern === options.scheduleAt;
|
||||
|
||||
if (isSameJob) {
|
||||
await queue.removeRepeatableByKey(repeatableJob.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const job: Job | undefined =
|
||||
await this.getQueue(queueName).getJob(sanitizedJobId);
|
||||
const job: Job | undefined = await queue.getJob(sanitizedJobId);
|
||||
|
||||
if (job) {
|
||||
await job.remove();
|
||||
@@ -211,9 +226,7 @@ export default class Queue {
|
||||
|
||||
if (options?.repeatableKey) {
|
||||
// remove existing repeatable job
|
||||
await this.getQueue(queueName).removeRepeatableByKey(
|
||||
options?.repeatableKey,
|
||||
);
|
||||
await queue.removeRepeatableByKey(options?.repeatableKey);
|
||||
}
|
||||
|
||||
// Store repeatable jobs for re-adding on reconnect
|
||||
@@ -228,11 +241,7 @@ export default class Queue {
|
||||
};
|
||||
}
|
||||
|
||||
const jobAdded: Job = await this.getQueue(queueName).add(
|
||||
jobName,
|
||||
data,
|
||||
optionsObject,
|
||||
);
|
||||
const jobAdded: Job = await queue.add(jobName, data, optionsObject);
|
||||
|
||||
return jobAdded;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import { JSONObject } from "../../Types/JSON";
|
||||
import MonitorType, {
|
||||
MonitorTypeHelper,
|
||||
} from "../../Types/Monitor/MonitorType";
|
||||
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
|
||||
import MonitorStep from "../../Types/Monitor/MonitorStep";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import Typeof from "../../Types/Typeof";
|
||||
@@ -71,11 +73,61 @@ import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil
|
||||
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
||||
import MetricService from "./MetricService";
|
||||
|
||||
export interface MonitorDestinationInfo {
|
||||
monitorDestination: string;
|
||||
requestType: string;
|
||||
monitorType: string;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
public getMonitorDestinationInfo(monitor: Model): MonitorDestinationInfo {
|
||||
let monitorDestination: string = "";
|
||||
let requestType: string = "";
|
||||
const monitorType: MonitorType | undefined = monitor.monitorType;
|
||||
|
||||
if (monitor.monitorSteps) {
|
||||
const monitorSteps: MonitorSteps = monitor.monitorSteps;
|
||||
const stepsArray: Array<MonitorStep> =
|
||||
monitorSteps.data?.monitorStepsInstanceArray || [];
|
||||
|
||||
if (stepsArray.length > 0) {
|
||||
const firstStep: MonitorStep | undefined = stepsArray[0];
|
||||
|
||||
// Get monitor destination
|
||||
if (firstStep?.data?.monitorDestination) {
|
||||
monitorDestination =
|
||||
firstStep.data.monitorDestination.toString() || "";
|
||||
}
|
||||
|
||||
// Get request type for API monitors
|
||||
if (monitorType === MonitorType.API && firstStep?.data?.requestType) {
|
||||
requestType = firstStep.data.requestType;
|
||||
}
|
||||
|
||||
// For port monitors, append port to destination
|
||||
if (
|
||||
monitorType === MonitorType.Port &&
|
||||
firstStep?.data?.monitorDestinationPort
|
||||
) {
|
||||
const port: string = firstStep.data.monitorDestinationPort.toString();
|
||||
if (monitorDestination && port) {
|
||||
monitorDestination = `${monitorDestination}:${port}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
monitorDestination,
|
||||
requestType,
|
||||
monitorType: monitorType || "",
|
||||
};
|
||||
}
|
||||
|
||||
public async refreshMonitorCurrentStatus(monitorId: ObjectID): Promise<void> {
|
||||
const monitor: Model | null = await this.findOneById({
|
||||
id: monitorId,
|
||||
@@ -1135,6 +1187,8 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
name: true,
|
||||
},
|
||||
description: true,
|
||||
monitorType: true,
|
||||
monitorSteps: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -1172,6 +1226,10 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
? "Disabled"
|
||||
: "Enabled";
|
||||
|
||||
// Get monitor destination info using the helper function
|
||||
const destinationInfo: MonitorDestinationInfo =
|
||||
this.getMonitorDestinationInfo(monitor);
|
||||
|
||||
const vars: Dictionary<string> = {
|
||||
title: title,
|
||||
monitorName: monitor.name!,
|
||||
@@ -1184,6 +1242,9 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
monitorViewLink: (
|
||||
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
|
||||
).toString(),
|
||||
monitorDestination: destinationInfo.monitorDestination,
|
||||
requestType: destinationInfo.requestType,
|
||||
monitorType: destinationInfo.monitorType,
|
||||
};
|
||||
|
||||
if (doesResourceHasOwners === true) {
|
||||
@@ -1259,6 +1320,8 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
name: true,
|
||||
},
|
||||
description: true,
|
||||
monitorType: true,
|
||||
monitorSteps: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -1292,6 +1355,10 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
? "Disconnected"
|
||||
: "Connected";
|
||||
|
||||
// Get monitor destination info using the helper function
|
||||
const destinationInfo: MonitorDestinationInfo =
|
||||
this.getMonitorDestinationInfo(monitor);
|
||||
|
||||
const vars: Dictionary<string> = {
|
||||
title: `Probes for monitor ${monitor.name} is ${status}.`,
|
||||
monitorName: monitor.name!,
|
||||
@@ -1304,6 +1371,9 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
monitorViewLink: (
|
||||
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
|
||||
).toString(),
|
||||
monitorDestination: destinationInfo.monitorDestination,
|
||||
requestType: destinationInfo.requestType,
|
||||
monitorType: destinationInfo.monitorType,
|
||||
};
|
||||
|
||||
if (doesResourceHasOwners === true) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import MonitorEvaluationSummary, {
|
||||
} from "../../../Types/Monitor/MonitorEvaluationSummary";
|
||||
import ProbeApiIngestResponse from "../../../Types/Probe/ProbeApiIngestResponse";
|
||||
import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
|
||||
import RequestFailedDetails from "../../../Types/Probe/RequestFailedDetails";
|
||||
import IncomingMonitorRequest from "../../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
||||
import MonitorType from "../../../Types/Monitor/MonitorType";
|
||||
import { CheckOn, CriteriaFilter } from "../../../Types/Monitor/CriteriaFilter";
|
||||
@@ -472,6 +473,7 @@ ${contextBlock}
|
||||
}): string | null {
|
||||
const requestDetails: Array<string> = [];
|
||||
const responseDetails: Array<string> = [];
|
||||
const failureDetails: Array<string> = [];
|
||||
|
||||
const probeResponse: ProbeMonitorResponse | null =
|
||||
MonitorCriteriaDataExtractor.getProbeMonitorResponse(input.dataToProcess);
|
||||
@@ -526,6 +528,34 @@ ${contextBlock}
|
||||
);
|
||||
}
|
||||
|
||||
// Add Request Failed Details if available
|
||||
if (probeResponse?.requestFailedDetails) {
|
||||
const requestFailedDetails: RequestFailedDetails =
|
||||
probeResponse.requestFailedDetails;
|
||||
|
||||
if (requestFailedDetails.failedPhase) {
|
||||
failureDetails.push(
|
||||
`- Failed Phase: ${requestFailedDetails.failedPhase}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (requestFailedDetails.errorCode) {
|
||||
failureDetails.push(`- Error Code: ${requestFailedDetails.errorCode}`);
|
||||
}
|
||||
|
||||
if (requestFailedDetails.errorDescription) {
|
||||
failureDetails.push(
|
||||
`- Error Description: ${requestFailedDetails.errorDescription}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (requestFailedDetails.rawErrorMessage) {
|
||||
failureDetails.push(
|
||||
`- Raw Error Message: ${requestFailedDetails.rawErrorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sections: Array<string> = [];
|
||||
|
||||
if (requestDetails.length > 0) {
|
||||
@@ -536,6 +566,12 @@ ${contextBlock}
|
||||
sections.push(`\n\n**Response Snapshot**\n${responseDetails.join("\n")}`);
|
||||
}
|
||||
|
||||
if (failureDetails.length > 0) {
|
||||
sections.push(
|
||||
`\n\n**Request Failed Details**\n${failureDetails.join("\n")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!sections.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,22 @@ export default class PushNotificationUtil {
|
||||
incidentTitle: string;
|
||||
projectName: string;
|
||||
newState: string;
|
||||
previousState?: string;
|
||||
incidentViewLink: string;
|
||||
}): PushNotificationMessage {
|
||||
const { incidentTitle, projectName, newState, incidentViewLink } = params;
|
||||
const {
|
||||
incidentTitle,
|
||||
projectName,
|
||||
newState,
|
||||
previousState,
|
||||
incidentViewLink,
|
||||
} = params;
|
||||
const stateChangeText: string = previousState
|
||||
? `Incident state changed from ${previousState} to ${newState}`
|
||||
: `Incident state changed to ${newState}`;
|
||||
return PushNotificationUtil.applyDefaults({
|
||||
title: `Incident Updated: ${incidentTitle}`,
|
||||
body: `Incident state changed to ${newState} in ${projectName}. Click to view details.`,
|
||||
body: `${stateChangeText} in ${projectName}. Click to view details.`,
|
||||
clickAction: incidentViewLink,
|
||||
url: incidentViewLink,
|
||||
tag: "incident-state-changed",
|
||||
@@ -56,6 +66,7 @@ export default class PushNotificationUtil {
|
||||
incidentTitle: incidentTitle,
|
||||
projectName: projectName,
|
||||
newState: newState,
|
||||
previousState: previousState,
|
||||
url: incidentViewLink,
|
||||
},
|
||||
});
|
||||
@@ -113,12 +124,22 @@ export default class PushNotificationUtil {
|
||||
monitorName: string;
|
||||
projectName: string;
|
||||
newStatus: string;
|
||||
previousStatus?: string;
|
||||
monitorViewLink: string;
|
||||
}): PushNotificationMessage {
|
||||
const { monitorName, projectName, newStatus, monitorViewLink } = params;
|
||||
const {
|
||||
monitorName,
|
||||
projectName,
|
||||
newStatus,
|
||||
previousStatus,
|
||||
monitorViewLink,
|
||||
} = params;
|
||||
const statusChangeText: string = previousStatus
|
||||
? `Monitor status changed from ${previousStatus} to ${newStatus}`
|
||||
: `Monitor status changed to ${newStatus}`;
|
||||
return PushNotificationUtil.applyDefaults({
|
||||
title: `Monitor ${newStatus}: ${monitorName}`,
|
||||
body: `Monitor status changed to ${newStatus} in ${projectName}. Click to view details.`,
|
||||
body: `${statusChangeText} in ${projectName}. Click to view details.`,
|
||||
clickAction: monitorViewLink,
|
||||
url: monitorViewLink,
|
||||
tag: "monitor-status-changed",
|
||||
@@ -128,6 +149,7 @@ export default class PushNotificationUtil {
|
||||
monitorName: monitorName,
|
||||
projectName: projectName,
|
||||
newStatus: newStatus,
|
||||
previousStatus: previousStatus,
|
||||
url: monitorViewLink,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("Loader tests", () => {
|
||||
loaderType={LoaderType.Bar}
|
||||
/>,
|
||||
);
|
||||
const barLoader: HTMLElement = screen.getByRole("bar-loader");
|
||||
const barLoader: HTMLElement = screen.getByTestId("bar-loader");
|
||||
expect(barLoader).toBeInTheDocument();
|
||||
});
|
||||
test("it should render if beats loader show up", () => {
|
||||
@@ -25,7 +25,7 @@ describe("Loader tests", () => {
|
||||
loaderType={LoaderType.Beats}
|
||||
/>,
|
||||
);
|
||||
const beatLoader: HTMLElement = screen.getByRole("beat-loader");
|
||||
const beatLoader: HTMLElement = screen.getByTestId("beat-loader");
|
||||
expect(beatLoader).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DashboardBaseComponent from "./DashboardBaseComponent";
|
||||
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
|
||||
|
||||
export enum ComponentInputType {
|
||||
Text = "Text",
|
||||
@@ -9,6 +10,7 @@ export enum ComponentInputType {
|
||||
Decimal = "Decimal",
|
||||
MetricsQueryConfig = "MetricsQueryConfig",
|
||||
LongText = "Long Text",
|
||||
Dropdown = "Dropdown",
|
||||
}
|
||||
|
||||
export interface ComponentArgument<T extends DashboardBaseComponent> {
|
||||
@@ -19,4 +21,5 @@ export interface ComponentArgument<T extends DashboardBaseComponent> {
|
||||
id: keyof T["arguments"];
|
||||
isAdvanced?: boolean | undefined;
|
||||
placeholder?: string | undefined;
|
||||
dropdownOptions?: Array<DropdownOption> | undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MetricQueryConfigData from "../../Metrics/MetricQueryConfigData";
|
||||
import ObjectID from "../../ObjectID";
|
||||
import DashboardComponentType from "../DashboardComponentType";
|
||||
import DashboardChartType from "../Chart/ChartType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export default interface DashboardChartComponent extends BaseComponent {
|
||||
@@ -12,5 +13,6 @@ export default interface DashboardChartComponent extends BaseComponent {
|
||||
chartDescription?: string | undefined;
|
||||
legendText?: string | undefined;
|
||||
legendUnit?: string | undefined;
|
||||
chartType?: DashboardChartType | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import AggregatedModel from "../BaseDatabase/AggregatedModel";
|
||||
import MetricAliasData from "./MetricAliasData";
|
||||
import MetricQueryData from "./MetricQueryData";
|
||||
|
||||
export enum MetricChartType {
|
||||
LINE = "line",
|
||||
BAR = "bar",
|
||||
}
|
||||
|
||||
export interface ChartSeries {
|
||||
title: string;
|
||||
}
|
||||
@@ -10,4 +15,5 @@ export default interface MetricQueryConfigData {
|
||||
metricAliasData?: MetricAliasData | undefined;
|
||||
metricQueryData: MetricQueryData;
|
||||
getSeries?: ((data: AggregatedModel) => ChartSeries) | undefined;
|
||||
chartType?: MetricChartType | undefined;
|
||||
}
|
||||
|
||||
43
Common/Types/Monitor/NetworkMonitor/NetworkPathTrace.ts
Normal file
43
Common/Types/Monitor/NetworkMonitor/NetworkPathTrace.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Represents a single hop in a network traceroute
|
||||
*/
|
||||
export interface TraceRouteHop {
|
||||
hopNumber: number;
|
||||
address: string | undefined; // IP address of the hop, undefined if the hop timed out
|
||||
hostName: string | undefined; // Hostname of the hop if DNS lookup succeeded
|
||||
roundTripTimeInMS: number | undefined; // RTT in milliseconds, undefined if timed out
|
||||
isTimeout: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the result of a traceroute operation
|
||||
*/
|
||||
export interface TraceRoute {
|
||||
hops: Array<TraceRouteHop>;
|
||||
destinationAddress: string;
|
||||
destinationHostName: string | undefined;
|
||||
isComplete: boolean; // Whether the traceroute reached the destination
|
||||
totalHops: number;
|
||||
failedHop: number | undefined; // The hop number where the route failed, if any
|
||||
failureMessage: string | undefined; // Failure message at the failed hop
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents DNS resolution information
|
||||
*/
|
||||
export interface DNSLookupResult {
|
||||
hostName: string;
|
||||
resolvedAddresses: Array<string>; // List of resolved IP addresses
|
||||
resolvedInMS: number; // Time taken for DNS resolution
|
||||
isSuccess: boolean;
|
||||
errorMessage: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete network path diagnosis result combining DNS and traceroute
|
||||
*/
|
||||
export default interface NetworkPathTrace {
|
||||
dnsLookup?: DNSLookupResult | undefined;
|
||||
traceRoute?: TraceRoute | undefined;
|
||||
timestamp: Date;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import SyntheticMonitorResponse from "../Monitor/SyntheticMonitors/SyntheticMoni
|
||||
import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
|
||||
import ObjectID from "../ObjectID";
|
||||
import Port from "../Port";
|
||||
import RequestFailedDetails from "./RequestFailedDetails";
|
||||
|
||||
export default interface ProbeMonitorResponse {
|
||||
projectId: ObjectID;
|
||||
@@ -23,6 +24,7 @@ export default interface ProbeMonitorResponse {
|
||||
monitorId: ObjectID;
|
||||
probeId: ObjectID;
|
||||
failureCause: string;
|
||||
requestFailedDetails?: RequestFailedDetails | undefined;
|
||||
sslResponse?: SslMonitorResponse | undefined;
|
||||
syntheticMonitorResponse?: Array<SyntheticMonitorResponse> | undefined;
|
||||
customCodeMonitorResponse?: CustomCodeMonitorResponse | undefined;
|
||||
|
||||
36
Common/Types/Probe/RequestFailedDetails.ts
Normal file
36
Common/Types/Probe/RequestFailedDetails.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This type holds detailed error context for when a request fails
|
||||
* This helps users understand exactly where and why a request failed
|
||||
*/
|
||||
|
||||
export enum RequestFailedPhase {
|
||||
// DNS resolution failed - could not resolve hostname to IP
|
||||
DNSResolution = "DNS Resolution",
|
||||
// TCP connection failed - could not establish connection to server
|
||||
TCPConnection = "TCP Connection",
|
||||
// TLS/SSL handshake failed
|
||||
TLSHandshake = "TLS Handshake",
|
||||
// Request was sent but timed out waiting for response
|
||||
RequestTimeout = "Request Timeout",
|
||||
// Server responded with an error status code
|
||||
ServerResponse = "Server Response",
|
||||
// Request was aborted
|
||||
RequestAborted = "Request Aborted",
|
||||
// Network error - general network failure
|
||||
NetworkError = "Network Error",
|
||||
// Certificate error
|
||||
CertificateError = "Certificate Error",
|
||||
// Unknown error
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export default interface RequestFailedDetails {
|
||||
// The phase at which the request failed
|
||||
failedPhase: RequestFailedPhase;
|
||||
// The error code from axios/node (e.g., ECONNREFUSED, ETIMEDOUT, ENOTFOUND, etc.)
|
||||
errorCode?: string | undefined;
|
||||
// A detailed, user-friendly explanation of what went wrong
|
||||
errorDescription: string;
|
||||
// The raw error message (for debugging purposes)
|
||||
rawErrorMessage?: string | undefined;
|
||||
}
|
||||
76
Common/UI/Components/Charts/Bar/BarChart.tsx
Normal file
76
Common/UI/Components/Charts/Bar/BarChart.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { BarChart } from "../ChartLibrary/BarChart/BarChart";
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import SeriesPoint from "../Types/SeriesPoints";
|
||||
import { XAxis } from "../Types/XAxis/XAxis";
|
||||
import YAxis from "../Types/YAxis/YAxis";
|
||||
import ChartDataPoint from "../ChartLibrary/Types/ChartDataPoint";
|
||||
import DataPointUtil from "../Utils/DataPoint";
|
||||
|
||||
export interface ComponentProps {
|
||||
data: Array<SeriesPoint>;
|
||||
xAxis: XAxis;
|
||||
yAxis: YAxis;
|
||||
sync: boolean;
|
||||
heightInPx?: number | undefined;
|
||||
}
|
||||
|
||||
export interface BarInternalProps extends ComponentProps {
|
||||
syncid: string;
|
||||
}
|
||||
|
||||
const BarChartElement: FunctionComponent<BarInternalProps> = (
|
||||
props: BarInternalProps,
|
||||
): ReactElement => {
|
||||
const [records, setRecords] = React.useState<Array<ChartDataPoint>>([]);
|
||||
|
||||
const categories: Array<string> = props.data.map((item: SeriesPoint) => {
|
||||
return item.seriesName;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.data || props.data.length === 0) {
|
||||
setRecords([]);
|
||||
}
|
||||
|
||||
const records: Array<ChartDataPoint> = DataPointUtil.getChartDataPoints({
|
||||
seriesPoints: props.data,
|
||||
xAxis: props.xAxis,
|
||||
yAxis: props.yAxis,
|
||||
});
|
||||
|
||||
setRecords(records);
|
||||
}, [props.data]);
|
||||
|
||||
const className: string = props.heightInPx ? `` : "h-80";
|
||||
const style: React.CSSProperties = props.heightInPx
|
||||
? { height: `${props.heightInPx}px` }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
className={className}
|
||||
style={style}
|
||||
data={records}
|
||||
tickGap={1}
|
||||
index={"Time"}
|
||||
categories={categories}
|
||||
colors={[
|
||||
"indigo",
|
||||
"rose",
|
||||
"emerald",
|
||||
"amber",
|
||||
"cyan",
|
||||
"gray",
|
||||
"pink",
|
||||
"lime",
|
||||
"fuchsia",
|
||||
]}
|
||||
valueFormatter={props.yAxis.options.formatter || undefined}
|
||||
showTooltip={true}
|
||||
yAxisWidth={60}
|
||||
syncid={props.sync ? props.syncid : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarChartElement;
|
||||
@@ -1,5 +1,8 @@
|
||||
import Text from "../../../../Types/Text";
|
||||
import LineChart, { ComponentProps as LineChartProps } from "../Line/LineChart";
|
||||
import BarChartElement, {
|
||||
ComponentProps as BarChartProps,
|
||||
} from "../Bar/BarChart";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export enum ChartType {
|
||||
@@ -13,7 +16,7 @@ export interface Chart {
|
||||
title: string;
|
||||
description?: string | undefined;
|
||||
type: ChartType;
|
||||
props: LineChartProps;
|
||||
props: LineChartProps | BarChartProps;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
@@ -55,7 +58,36 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
|
||||
)}
|
||||
<LineChart
|
||||
key={index}
|
||||
{...chart.props}
|
||||
{...(chart.props as LineChartProps)}
|
||||
syncid={syncId}
|
||||
heightInPx={props.heightInPx}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case ChartType.BAR:
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-6 ${props.hideCard ? "" : "rounded-md bg-white shadow"} ${props.chartCssClass || ""}`}
|
||||
>
|
||||
<h2
|
||||
data-testid="card-details-heading"
|
||||
id="card-details-heading"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{chart.title}
|
||||
</h2>
|
||||
{chart.description && (
|
||||
<p
|
||||
data-testid="card-description"
|
||||
className="mt-1 text-sm text-gray-500 w-full hidden md:block"
|
||||
>
|
||||
{chart.description}
|
||||
</p>
|
||||
)}
|
||||
<BarChartElement
|
||||
key={index}
|
||||
{...(chart.props as BarChartProps)}
|
||||
syncid={syncId}
|
||||
heightInPx={props.heightInPx}
|
||||
/>
|
||||
|
||||
@@ -82,12 +82,42 @@ const renderShape: (
|
||||
width = Math.abs(width); // width must be a positive number
|
||||
}
|
||||
|
||||
// Radius for rounded corners at top
|
||||
const radius: number = Math.min(4, width / 2, height / 2);
|
||||
|
||||
/*
|
||||
* Create path with rounded corners at the top only (for horizontal layout)
|
||||
* For vertical layout, round the right side
|
||||
*/
|
||||
let path: string;
|
||||
|
||||
if (layout === "horizontal") {
|
||||
// Rounded top corners for horizontal bars
|
||||
path = `
|
||||
M ${x},${y + height}
|
||||
L ${x},${y + radius}
|
||||
Q ${x},${y} ${x + radius},${y}
|
||||
L ${x + width - radius},${y}
|
||||
Q ${x + width},${y} ${x + width},${y + radius}
|
||||
L ${x + width},${y + height}
|
||||
Z
|
||||
`;
|
||||
} else {
|
||||
// Rounded right corners for vertical bars
|
||||
path = `
|
||||
M ${x},${y}
|
||||
L ${x + width - radius},${y}
|
||||
Q ${x + width},${y} ${x + width},${y + radius}
|
||||
L ${x + width},${y + height - radius}
|
||||
Q ${x + width},${y + height} ${x + width - radius},${y + height}
|
||||
L ${x},${y + height}
|
||||
Z
|
||||
`;
|
||||
}
|
||||
|
||||
return (
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
<path
|
||||
d={path}
|
||||
opacity={
|
||||
activeBar || (activeLegend && activeLegend !== name)
|
||||
? deepEqual(activeBar, { ...payload, value })
|
||||
@@ -615,6 +645,7 @@ interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
legendPosition?: "left" | "center" | "right";
|
||||
tooltipCallback?: (tooltipCallbackContent: TooltipProps) => void;
|
||||
customTooltip?: React.ComponentType<TooltipProps>;
|
||||
syncid?: string | undefined;
|
||||
}
|
||||
|
||||
const BarChart: React.ForwardRefExoticComponent<
|
||||
@@ -753,6 +784,7 @@ const BarChart: React.ForwardRefExoticComponent<
|
||||
<ResponsiveContainer>
|
||||
<RechartsBarChart
|
||||
data={data}
|
||||
syncId={props.syncid?.toString() || ""}
|
||||
{...(hasOnValueChange && (activeLegend || activeBar)
|
||||
? {
|
||||
onClick: handleChartClick,
|
||||
|
||||
@@ -25,9 +25,9 @@ const Loader: FunctionComponent<ComponentProps> = ({
|
||||
if (loaderType === LoaderType.Bar) {
|
||||
return (
|
||||
<div
|
||||
role={`bar-loader mt-1 ${className}`}
|
||||
className="flex justify-center"
|
||||
data-testid="loader"
|
||||
role="presentation"
|
||||
className={`flex justify-center mt-1 ${className}`.trim()}
|
||||
data-testid="bar-loader"
|
||||
>
|
||||
<BarLoader height={4} width={size} color={color.toString()} />
|
||||
</div>
|
||||
@@ -37,9 +37,9 @@ const Loader: FunctionComponent<ComponentProps> = ({
|
||||
if (loaderType === LoaderType.Beats) {
|
||||
return (
|
||||
<div
|
||||
role="beat-loader mt-1"
|
||||
className="justify-center"
|
||||
data-testid="loader"
|
||||
role="presentation"
|
||||
className={`justify-center mt-1 ${className}`.trim()}
|
||||
data-testid="beat-loader"
|
||||
>
|
||||
<BeatLoader size={size} color={color.toString()} />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,9 @@ import URL from "../Types/API/URL";
|
||||
import Dictionary from "../Types/Dictionary";
|
||||
import APIException from "../Types/Exception/ApiException";
|
||||
import { JSONArray, JSONObject } from "../Types/JSON";
|
||||
import RequestFailedDetails, {
|
||||
RequestFailedPhase,
|
||||
} from "../Types/Probe/RequestFailedDetails";
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosProgressEvent,
|
||||
@@ -525,7 +528,49 @@ export default class API {
|
||||
// get url from error
|
||||
const url: string = error?.config?.url || "";
|
||||
|
||||
const errorMessage: string = error.message || error.toString();
|
||||
// Get a meaningful error message, avoiding generic "Error" strings
|
||||
let errorMessage: string = error.message || "";
|
||||
|
||||
// If error message is empty or just "Error", try to get more details from the error
|
||||
if (
|
||||
!errorMessage ||
|
||||
errorMessage.toLowerCase() === "error" ||
|
||||
errorMessage.trim() === ""
|
||||
) {
|
||||
// Check for common axios error codes
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
case "ECONNREFUSED":
|
||||
errorMessage = "Connection refused";
|
||||
break;
|
||||
case "ECONNRESET":
|
||||
errorMessage = "Connection reset";
|
||||
break;
|
||||
case "ETIMEDOUT":
|
||||
errorMessage = "Connection timed out";
|
||||
break;
|
||||
case "ENOTFOUND":
|
||||
errorMessage = "Host not found";
|
||||
break;
|
||||
case "ECONNABORTED":
|
||||
errorMessage = "Connection aborted";
|
||||
break;
|
||||
case "ERR_NETWORK":
|
||||
errorMessage = "Network error";
|
||||
break;
|
||||
case "ERR_BAD_REQUEST":
|
||||
errorMessage = "Bad request";
|
||||
break;
|
||||
case "ERR_BAD_RESPONSE":
|
||||
errorMessage = "Bad response from server";
|
||||
break;
|
||||
default:
|
||||
errorMessage = error.code || "Unknown error";
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Request failed";
|
||||
}
|
||||
}
|
||||
|
||||
throw new APIException(`Request failed to ${url}. ${errorMessage}`, error);
|
||||
}
|
||||
@@ -534,7 +579,37 @@ export default class API {
|
||||
let errorString: string = error.message || error.toString();
|
||||
|
||||
if (error instanceof APIException) {
|
||||
errorString = `${error.message?.toString()} ${error.error?.message || error.error?.toString() || ""}`;
|
||||
// Get the nested error message, but avoid duplicating or adding empty/generic messages
|
||||
let nestedErrorMessage: string = "";
|
||||
if (error.error) {
|
||||
// Get the error message, avoiding generic "Error" or empty strings
|
||||
const errMsg: string = error.error.message || "";
|
||||
const errStr: string = error.error.toString() || "";
|
||||
|
||||
// Check if the error message is meaningful (not just "Error" or empty)
|
||||
if (
|
||||
errMsg &&
|
||||
errMsg.trim().toLowerCase() !== "error" &&
|
||||
errMsg.trim() !== ""
|
||||
) {
|
||||
nestedErrorMessage = errMsg;
|
||||
} else if (
|
||||
errStr &&
|
||||
errStr.trim().toLowerCase() !== "error" &&
|
||||
errStr.trim().toLowerCase() !== "error:" &&
|
||||
errStr.trim() !== "" &&
|
||||
!errStr.toLowerCase().startsWith("error:")
|
||||
) {
|
||||
nestedErrorMessage = errStr;
|
||||
}
|
||||
}
|
||||
|
||||
// Only append nested error if it's meaningful and not already in the main message
|
||||
if (nestedErrorMessage && !error.message?.includes(nestedErrorMessage)) {
|
||||
errorString = `${error.message?.toString()} ${nestedErrorMessage}`;
|
||||
} else {
|
||||
errorString = error.message?.toString() || "";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle AggregateError by extracting the underlying error messages
|
||||
@@ -626,4 +701,232 @@ export default class API {
|
||||
|
||||
return errorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts detailed error information from an axios error or generic error.
|
||||
* This provides more context about where and why a request failed.
|
||||
*/
|
||||
public static getRequestFailedDetails(
|
||||
error: AxiosError | Error | unknown,
|
||||
): RequestFailedDetails {
|
||||
const axiosError: AxiosError | null = axios.isAxiosError(error)
|
||||
? (error as AxiosError)
|
||||
: null;
|
||||
const errorCode: string | undefined = axiosError?.code;
|
||||
const rawErrorMessage: string =
|
||||
(error as Error)?.message || String(error) || "Unknown error";
|
||||
|
||||
// Helper to determine the phase and description based on error code/message
|
||||
const lowerMessage: string = rawErrorMessage.toLowerCase();
|
||||
|
||||
// DNS resolution failures
|
||||
if (errorCode === "ENOTFOUND" || lowerMessage.includes("enotfound")) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.DNSResolution,
|
||||
errorCode: errorCode || "ENOTFOUND",
|
||||
errorDescription:
|
||||
"DNS resolution failed. The hostname could not be resolved to an IP address. Please verify the hostname is correct and that DNS is working properly.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Connection refused
|
||||
if (errorCode === "ECONNREFUSED" || lowerMessage.includes("econnrefused")) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.TCPConnection,
|
||||
errorCode: errorCode || "ECONNREFUSED",
|
||||
errorDescription:
|
||||
"Connection refused. The server actively refused the connection. This usually means no service is listening on the specified port, or a firewall is blocking the connection.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Connection reset
|
||||
if (
|
||||
errorCode === "ECONNRESET" ||
|
||||
lowerMessage.includes("econnreset") ||
|
||||
lowerMessage.includes("connection reset")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.TCPConnection,
|
||||
errorCode: errorCode || "ECONNRESET",
|
||||
errorDescription:
|
||||
"Connection reset. The connection was forcibly closed by the server or a network device. This can happen due to server restarts, load balancer timeouts, or network issues.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Connection aborted
|
||||
if (
|
||||
errorCode === "ECONNABORTED" ||
|
||||
lowerMessage.includes("econnaborted") ||
|
||||
lowerMessage.includes("connection aborted")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.RequestAborted,
|
||||
errorCode: errorCode || "ECONNABORTED",
|
||||
errorDescription:
|
||||
"Connection aborted. The request was aborted, possibly due to a timeout or the connection being closed unexpectedly.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Timeout errors
|
||||
if (
|
||||
errorCode === "ETIMEDOUT" ||
|
||||
errorCode === "ESOCKETTIMEDOUT" ||
|
||||
lowerMessage.includes("timeout") ||
|
||||
lowerMessage.includes("exceeded")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.RequestTimeout,
|
||||
errorCode: errorCode || "TIMEOUT",
|
||||
errorDescription:
|
||||
"Request timed out. The server did not respond within the allowed time. This could be due to network latency, server overload, or the server being unresponsive.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// SSL/TLS Certificate errors
|
||||
if (
|
||||
lowerMessage.includes("certificate has expired") ||
|
||||
lowerMessage.includes("cert_has_expired")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.CertificateError,
|
||||
errorCode: "CERT_HAS_EXPIRED",
|
||||
errorDescription:
|
||||
"SSL certificate has expired. The server's SSL certificate is no longer valid. The certificate needs to be renewed.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lowerMessage.includes("self-signed certificate") ||
|
||||
lowerMessage.includes("self signed certificate") ||
|
||||
lowerMessage.includes("depth_zero_self_signed_cert")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.CertificateError,
|
||||
errorCode: "SELF_SIGNED_CERT",
|
||||
errorDescription:
|
||||
"Self-signed certificate detected. The server is using a self-signed SSL certificate that is not trusted by default. Consider using a certificate from a trusted Certificate Authority.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lowerMessage.includes("certificate signed by unknown authority") ||
|
||||
lowerMessage.includes("unable_to_verify_leaf_signature") ||
|
||||
lowerMessage.includes("unable to verify")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.CertificateError,
|
||||
errorCode: "CERT_UNKNOWN_AUTHORITY",
|
||||
errorDescription:
|
||||
"SSL certificate signed by unknown authority. The certificate chain could not be verified against known Certificate Authorities.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lowerMessage.includes("ssl") ||
|
||||
lowerMessage.includes("tls") ||
|
||||
lowerMessage.includes("certificate") ||
|
||||
lowerMessage.includes("handshake")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.TLSHandshake,
|
||||
errorCode: errorCode || "TLS_ERROR",
|
||||
errorDescription:
|
||||
"TLS/SSL handshake failed. There was an error establishing a secure connection to the server. This could be due to certificate issues, protocol mismatches, or the server not supporting HTTPS.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Network errors
|
||||
if (lowerMessage.includes("network error") || errorCode === "ERR_NETWORK") {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.NetworkError,
|
||||
errorCode: errorCode || "NETWORK_ERROR",
|
||||
errorDescription:
|
||||
"Network error occurred. Unable to reach the server due to network connectivity issues. Please check your network connection and ensure the server is accessible.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Host unreachable
|
||||
if (
|
||||
errorCode === "EHOSTUNREACH" ||
|
||||
lowerMessage.includes("host unreachable")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.TCPConnection,
|
||||
errorCode: errorCode || "EHOSTUNREACH",
|
||||
errorDescription:
|
||||
"Host unreachable. The network path to the server could not be found. This may be due to routing issues or the host being offline.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Network unreachable
|
||||
if (
|
||||
errorCode === "ENETUNREACH" ||
|
||||
lowerMessage.includes("network unreachable")
|
||||
) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.TCPConnection,
|
||||
errorCode: errorCode || "ENETUNREACH",
|
||||
errorDescription:
|
||||
"Network unreachable. There is no route to the network where the server resides. This is typically a routing or connectivity issue.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Server responded with error status
|
||||
if (axiosError?.response) {
|
||||
const status: number = axiosError.response.status;
|
||||
let description: string = `Server responded with HTTP status ${status}.`;
|
||||
|
||||
if (status >= 500) {
|
||||
description += " This indicates a server-side error.";
|
||||
} else if (status === 404) {
|
||||
description += " The requested resource was not found.";
|
||||
} else if (status === 403) {
|
||||
description += " Access to the resource is forbidden.";
|
||||
} else if (status === 401) {
|
||||
description += " Authentication is required or has failed.";
|
||||
} else if (status === 400) {
|
||||
description += " The request was malformed or invalid.";
|
||||
} else if (status >= 400) {
|
||||
description += " This indicates a client-side error.";
|
||||
}
|
||||
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.ServerResponse,
|
||||
errorCode: `HTTP_${status}`,
|
||||
errorDescription: description,
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Request was made but no response received
|
||||
if (axiosError?.request && !axiosError?.response) {
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.NetworkError,
|
||||
errorCode: errorCode || "NO_RESPONSE",
|
||||
errorDescription:
|
||||
"No response received from the server. The request was sent but no response was returned. This could indicate the server is down, unreachable, or the request timed out.",
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// Default/Unknown error
|
||||
return {
|
||||
failedPhase: RequestFailedPhase.Unknown,
|
||||
errorCode: errorCode,
|
||||
errorDescription: `Request failed: ${API.getFriendlyErrorMessage(error as Error)}`,
|
||||
rawErrorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ComponentInputType,
|
||||
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
import DashboardChartType from "../../../Types/Dashboard/Chart/ChartType";
|
||||
|
||||
export default class DashboardChartComponentUtil extends DashboardBaseComponentUtil {
|
||||
public static override getDefaultComponent(): DashboardChartComponent {
|
||||
@@ -27,6 +28,7 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
|
||||
groupBy: undefined,
|
||||
},
|
||||
},
|
||||
chartType: DashboardChartType.Line,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -38,6 +40,24 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
|
||||
ComponentArgument<DashboardChartComponent>
|
||||
> = [];
|
||||
|
||||
componentArguments.push({
|
||||
name: "Chart Type",
|
||||
description: "Select the type of chart to display",
|
||||
required: true,
|
||||
type: ComponentInputType.Dropdown,
|
||||
id: "chartType",
|
||||
dropdownOptions: [
|
||||
{
|
||||
label: "Line Chart",
|
||||
value: DashboardChartType.Line,
|
||||
},
|
||||
{
|
||||
label: "Bar Chart",
|
||||
value: DashboardChartType.Bar,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Chart Configuration",
|
||||
description: "Please select the metrics to display on the chart",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"transform": {
|
||||
".(ts|tsx)": "ts-jest"
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"testEnvironment": "jsdom",
|
||||
"collectCoverage": false,
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
|
||||
24
Common/package-lock.json
generated
24
Common/package-lock.json
generated
@@ -11458,23 +11458,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -17357,12 +17357,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/web-push/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
|
||||
49
Copilot/package-lock.json
generated
49
Copilot/package-lock.json
generated
@@ -19,7 +19,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.45",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -301,19 +300,6 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -321,26 +307,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -385,21 +351,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json5": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"strip-bom": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"compile": "tsc",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"dev": "ts-node --transpile-only -r tsconfig-paths/register src/Index.ts",
|
||||
"start": "node --enable-source-maps ./build/dist/Index.js",
|
||||
"clear-modules": "rm -rf node_modules && rm -f package-lock.json && npm install"
|
||||
@@ -21,7 +22,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.45",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
|
||||
16
Dashboard/package-lock.json
generated
16
Dashboard/package-lock.json
generated
@@ -12532,12 +12532,12 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -18586,12 +18586,12 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/web-push/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"ejs": "^3.1.10",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-router-dom": "^6.30.2",
|
||||
"reactflow": "^11.11.2",
|
||||
"stripe": "^11.0.0",
|
||||
"use-async-effect": "^2.2.6"
|
||||
|
||||
@@ -160,6 +160,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
|
||||
placeholder: arg.placeholder,
|
||||
...ComponentInputTypeToFormFieldType.getFormFieldTypeByComponentInputType(
|
||||
arg.type,
|
||||
arg.dropdownOptions,
|
||||
),
|
||||
getCustomElement: getCustomElememnt(arg),
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchem
|
||||
export default class ComponentInputTypeToFormFieldType {
|
||||
public static getFormFieldTypeByComponentInputType(
|
||||
componentInputType: ComponentInputType,
|
||||
dropdownOptions?: Array<DropdownOption> | undefined,
|
||||
): {
|
||||
fieldType: FormFieldSchemaType;
|
||||
dropdownOptions?: Array<DropdownOption> | undefined;
|
||||
@@ -52,6 +53,13 @@ export default class ComponentInputTypeToFormFieldType {
|
||||
};
|
||||
}
|
||||
|
||||
if (componentInputType === ComponentInputType.Dropdown) {
|
||||
return {
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownOptions: dropdownOptions || [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
dropdownOptions: [],
|
||||
|
||||
@@ -10,10 +10,13 @@ import MetricUtil from "../../Metrics/Utils/Metrics";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import MetricQueryConfigData, {
|
||||
MetricChartType,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import DashboardChartType from "Common/Types/Dashboard/Chart/ChartType";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardChartComponent;
|
||||
@@ -141,6 +144,16 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
// add title and description.
|
||||
|
||||
type GetMetricChartType = () => MetricChartType;
|
||||
|
||||
// Convert dashboard chart type to metric chart type
|
||||
const getMetricChartType: GetMetricChartType = (): MetricChartType => {
|
||||
if (props.component.arguments.chartType === DashboardChartType.Bar) {
|
||||
return MetricChartType.BAR;
|
||||
}
|
||||
return MetricChartType.LINE;
|
||||
};
|
||||
|
||||
const chartMetricViewData: MetricViewData = {
|
||||
queryConfigs: props.component.arguments.metricQueryConfig
|
||||
? [
|
||||
@@ -154,6 +167,7 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
legend: props.component.arguments.legendText || undefined,
|
||||
legendUnit: props.component.arguments.legendUnit || undefined,
|
||||
},
|
||||
chartType: getMetricChartType(),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
|
||||
@@ -10,7 +10,10 @@ import { XAxisAggregateType } from "Common/UI/Components/Charts/Types/XAxis/XAxi
|
||||
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
|
||||
import SeriesPoint from "Common/UI/Components/Charts/Types/SeriesPoints";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import {
|
||||
ChartSeries,
|
||||
MetricChartType,
|
||||
} from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregatedModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import YAxisType from "Common/UI/Components/Charts/Types/YAxis/YAxisType";
|
||||
import { YAxisPrecision } from "Common/UI/Components/Charts/Types/YAxis/YAxis";
|
||||
@@ -156,9 +159,15 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
|
||||
// Determine chart type - use BAR for bar chart type, LINE for everything else
|
||||
const chartType: ChartType =
|
||||
queryConfig.chartType === MetricChartType.BAR
|
||||
? ChartType.BAR
|
||||
: ChartType.LINE;
|
||||
|
||||
const chart: Chart = {
|
||||
id: index.toString(),
|
||||
type: ChartType.LINE,
|
||||
type: chartType,
|
||||
title:
|
||||
queryConfig.metricAliasData?.title ||
|
||||
queryConfig.metricQueryData.filterData.metricName?.toString() ||
|
||||
|
||||
@@ -17,6 +17,11 @@ const PingMonitorView: FunctionComponent<ComponentProps> = (
|
||||
responseTimeInMs = Math.round(responseTimeInMs);
|
||||
}
|
||||
|
||||
// Check if there are error details to show
|
||||
const hasErrorDetails: boolean = Boolean(
|
||||
props.probeMonitorResponse.requestFailedDetails,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex space-x-3">
|
||||
@@ -55,6 +60,50 @@ const PingMonitorView: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{props.probeMonitorResponse.failureCause && (
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-full shadow-none border-2 border-gray-100 "
|
||||
title="Error"
|
||||
value={props.probeMonitorResponse.failureCause?.toString() || "-"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Details Section */}
|
||||
{hasErrorDetails && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-1/2 shadow-none border-2 border-gray-100 "
|
||||
title="Failed At"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails?.failedPhase ||
|
||||
"-"
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/2 shadow-none border-2 border-gray-100 "
|
||||
title="Error Code"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails?.errorCode ||
|
||||
"-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-full shadow-none border-2 border-gray-100 "
|
||||
title="Error Details"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails
|
||||
?.errorDescription || "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,11 @@ const WebsiteMonitorSummaryView: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there are error details to show
|
||||
const hasErrorDetails: boolean = Boolean(
|
||||
props.probeMonitorResponse.requestFailedDetails,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex space-x-3">
|
||||
@@ -88,6 +93,40 @@ const WebsiteMonitorSummaryView: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Details Section */}
|
||||
{hasErrorDetails && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-1/2 shadow-none border-2 border-gray-100 "
|
||||
title="Failed At"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails?.failedPhase ||
|
||||
"-"
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/2 shadow-none border-2 border-gray-100 "
|
||||
title="Error Code"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails?.errorCode ||
|
||||
"-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-full shadow-none border-2 border-gray-100 "
|
||||
title="Error Details"
|
||||
value={
|
||||
props.probeMonitorResponse.requestFailedDetails
|
||||
?.errorDescription || "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showMoreDetails && fields.length > 0 && (
|
||||
<div>
|
||||
<Detail<ProbeMonitorResponse>
|
||||
|
||||
@@ -63,8 +63,7 @@ const POSTMORTEM_FORM_FIELDS: Fields<Incident> = [
|
||||
title: "Postmortem Published At",
|
||||
fieldType: FormFieldSchemaType.DateTime,
|
||||
required: false,
|
||||
description:
|
||||
"Set the posted-on timestamp subscribers will see. ",
|
||||
description: "Set the posted-on timestamp subscribers will see. ",
|
||||
placeholder: "Select date and time",
|
||||
getDefaultValue: () => {
|
||||
return OneUptimeDate.getCurrentDate();
|
||||
@@ -86,7 +85,7 @@ const POSTMORTEM_FORM_FIELDS: Fields<Incident> = [
|
||||
notifySubscribersOnPostmortemPublished: true,
|
||||
},
|
||||
title: "Notify Subscribers",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
required: false,
|
||||
description: "Notify subscribers when this postmortem is published.",
|
||||
defaultValue: true,
|
||||
@@ -259,7 +258,7 @@ const IncidentPostmortem: FunctionComponent<
|
||||
field: {
|
||||
showPostmortemOnStatusPage: true,
|
||||
},
|
||||
title: "Visible on Status Page?",
|
||||
title: "Postmortem visible on Status Page?",
|
||||
fieldType: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
@@ -268,6 +267,9 @@ const IncidentPostmortem: FunctionComponent<
|
||||
},
|
||||
title: "Notify Subscribers",
|
||||
fieldType: FieldType.Boolean,
|
||||
showIf: (item: Incident): boolean => {
|
||||
return Boolean(item.showPostmortemOnStatusPage);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
||||
159
E2E/README.md
Normal file
159
E2E/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# E2E Tests
|
||||
|
||||
End-to-end tests for OneUptime using [Playwright](https://playwright.dev/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v18 or higher recommended)
|
||||
- npm
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd E2E
|
||||
npm install
|
||||
```
|
||||
|
||||
This will automatically install Playwright browsers and dependencies via the `preinstall` script.
|
||||
|
||||
## Configuration
|
||||
|
||||
The tests use environment variables for configuration. Set the following variables before running tests in config.env:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HOST` | The hostname to test against | `localhost` |
|
||||
| `HTTP_PROTOCOL` | Protocol to use (`http` or `https`) | `http` |
|
||||
| `BILLING_ENABLED` | Enable billing-related tests | `false` |
|
||||
| `E2E_TEST_IS_USER_REGISTERED` | Whether a test user is already registered | `false` |
|
||||
| `E2E_TEST_REGISTERED_USER_EMAIL` | Email of the registered test user | - |
|
||||
| `E2E_TEST_REGISTERED_USER_PASSWORD` | Password of the registered test user | - |
|
||||
| `E2E_TEST_STATUS_PAGE_URL` | URL of a status page to test | - |
|
||||
| `E2E_TESTS_FAILED_WEBHOOK_URL` | Webhook URL to call on test failure | - |
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
export HOST=staging.oneuptime.com
|
||||
export HTTP_PROTOCOL=https
|
||||
export BILLING_ENABLED=true
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Run tests in debug mode
|
||||
|
||||
```bash
|
||||
npm run debug-tests
|
||||
```
|
||||
|
||||
### Run specific test file
|
||||
|
||||
```bash
|
||||
npx playwright test Tests/Home/Landing.spec.ts
|
||||
```
|
||||
|
||||
### Run tests for a specific browser
|
||||
|
||||
```bash
|
||||
# Chromium only
|
||||
npx playwright test --project=chromium
|
||||
|
||||
# Firefox only
|
||||
npx playwright test --project=firefox
|
||||
```
|
||||
|
||||
### Run tests with UI mode
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
### Run a specific test by name
|
||||
|
||||
```bash
|
||||
npx playwright test -g "oneUptime link navigate to homepage"
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
E2E/
|
||||
├── Tests/
|
||||
│ ├── Accounts/ # Account-related tests (login, registration)
|
||||
│ ├── App/ # Main application tests
|
||||
│ ├── Home/ # Homepage tests
|
||||
│ ├── IncomingRequestIngest/
|
||||
│ ├── ProbeIngest/
|
||||
│ ├── StatusPage/ # Status page tests
|
||||
│ └── TelemetryIngest/
|
||||
├── Config.ts # Environment configuration
|
||||
├── playwright.config.ts # Playwright configuration
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Viewing Test Reports
|
||||
|
||||
After running tests, an HTML report is generated. Open it with:
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
The Playwright configuration (`playwright.config.ts`) includes:
|
||||
|
||||
- **Timeout**: 240 seconds per test
|
||||
- **Retries**: 3 retries on failure
|
||||
- **Browsers**: Chromium and Firefox
|
||||
- **Tracing**: Enabled for debugging failed tests
|
||||
|
||||
## Debugging
|
||||
|
||||
### View traces
|
||||
|
||||
When tests fail, traces are collected. View them with:
|
||||
|
||||
```bash
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
### Run in headed mode
|
||||
|
||||
```bash
|
||||
npx playwright test --headed
|
||||
```
|
||||
|
||||
### Slow down execution
|
||||
|
||||
```bash
|
||||
npx playwright test --headed --slow-mo=1000
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
For CI environments, the `CI` environment variable is automatically detected:
|
||||
- `test.only` usage will fail the build
|
||||
- Parallel test execution is disabled (workers: 1)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Playwright browsers not installed
|
||||
|
||||
```bash
|
||||
npx playwright install
|
||||
npx playwright install-deps
|
||||
```
|
||||
|
||||
### Clear and reinstall dependencies
|
||||
|
||||
```bash
|
||||
npm run clear-modules
|
||||
```
|
||||
@@ -28,7 +28,10 @@ test.describe("check if pages loades with its title", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await page.getByRole("link", { name: "OneUptime", exact: true }).click();
|
||||
await page
|
||||
.getByRole("link", { name: /OneUptime/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
URL.fromString(BASE_URL.toString()).toString(),
|
||||
|
||||
101
Home/Routes.ts
101
Home/Routes.ts
@@ -4,6 +4,7 @@ import { StaticPath, ViewsPath } from "./Utils/Config";
|
||||
import NotFoundUtil from "./Utils/NotFound";
|
||||
import ProductCompare, { Product } from "./Utils/ProductCompare";
|
||||
import generateSitemapXml from "./Utils/Sitemap";
|
||||
import { getPageSEO, PageSEOData } from "./Utils/PageSEO";
|
||||
import DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
@@ -28,16 +29,32 @@ import "./Jobs/UpdateBlog";
|
||||
import { Host, IsBillingEnabled } from "Common/Server/EnvironmentConfig";
|
||||
import LocalCache from "Common/Server/Infrastructure/LocalCache";
|
||||
|
||||
// Helper to get SEO data and merge with homeUrl for templates
|
||||
const getSEOForPath: (
|
||||
path: string,
|
||||
homeUrl: string,
|
||||
) => PageSEOData & { fullCanonicalUrl: string } = (
|
||||
path: string,
|
||||
homeUrl: string,
|
||||
): PageSEOData & { fullCanonicalUrl: string } => {
|
||||
const seo: PageSEOData = getPageSEO(path);
|
||||
const baseUrl: string = homeUrl.replace(/\/$/, "");
|
||||
return {
|
||||
...seo,
|
||||
fullCanonicalUrl: `${baseUrl}${seo.canonicalPath}`,
|
||||
};
|
||||
};
|
||||
|
||||
const HomeFeatureSet: FeatureSet = {
|
||||
init: async (): Promise<void> => {
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
/*
|
||||
* Routes
|
||||
* Middleware to inject baseUrl for templates (used for canonical links)
|
||||
* Middleware to inject baseUrl and SEO data for templates
|
||||
*/
|
||||
app.use(
|
||||
async (_req: ExpressRequest, res: ExpressResponse, next: () => void) => {
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: () => void) => {
|
||||
if (!res.locals["homeUrl"]) {
|
||||
try {
|
||||
// Try to get cached home URL first.
|
||||
@@ -59,12 +76,21 @@ const HomeFeatureSet: FeatureSet = {
|
||||
res.locals["homeUrl"] = "https://oneuptime.com";
|
||||
}
|
||||
}
|
||||
// Inject SEO data for current path
|
||||
res.locals["seo"] = getSEOForPath(
|
||||
req.path,
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
next();
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const { reviewsList1, reviewsList2, reviewsList3 } = Reviews;
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
|
||||
res.render(`${ViewsPath}/index`, {
|
||||
support: false,
|
||||
@@ -76,6 +102,7 @@ const HomeFeatureSet: FeatureSet = {
|
||||
reviewsList1,
|
||||
reviewsList2,
|
||||
reviewsList3,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,14 +117,23 @@ const HomeFeatureSet: FeatureSet = {
|
||||
);
|
||||
|
||||
app.get("/support", async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/support",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/support`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/oss-friends",
|
||||
async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/oss-friends",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/oss-friends`, {
|
||||
ossFriends: OSSFriends.map((friend: OSSFriend) => {
|
||||
return {
|
||||
@@ -106,6 +142,7 @@ const HomeFeatureSet: FeatureSet = {
|
||||
};
|
||||
}),
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -877,15 +914,24 @@ const HomeFeatureSet: FeatureSet = {
|
||||
},
|
||||
];
|
||||
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/pricing",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/pricing`, {
|
||||
pricing,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/enterprise/demo",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/enterprise/demo",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/demo`, {
|
||||
support: false,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
@@ -893,6 +939,7 @@ const HomeFeatureSet: FeatureSet = {
|
||||
cta: false,
|
||||
blackLogo: true,
|
||||
requestDemoCta: false,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -900,8 +947,13 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/status-page",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/status-page",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/status-page`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -909,15 +961,25 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/logs-management",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/logs-management",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/logs-management`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/product/apm", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/apm",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/apm`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -999,11 +1061,16 @@ const HomeFeatureSet: FeatureSet = {
|
||||
gitHubCommits = commits;
|
||||
}
|
||||
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/about",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/about`, {
|
||||
contributors: gitHubContributors,
|
||||
basicInfo: gitHubBasicInfo,
|
||||
commits: gitHubCommits,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1038,8 +1105,13 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/monitoring",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/monitoring",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/monitoring`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1047,8 +1119,13 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/on-call",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/on-call",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/on-call`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1056,8 +1133,13 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/workflows",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/workflows",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/workflows`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1065,8 +1147,13 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/product/incident-management",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/product/incident-management",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/incident-management`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1081,6 +1168,10 @@ const HomeFeatureSet: FeatureSet = {
|
||||
app.get(
|
||||
"/enterprise/overview",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/enterprise/overview",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/enterprise-overview.ejs`, {
|
||||
support: false,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
@@ -1088,6 +1179,7 @@ const HomeFeatureSet: FeatureSet = {
|
||||
cta: true,
|
||||
blackLogo: false,
|
||||
requestDemoCta: true,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1366,6 +1458,10 @@ const HomeFeatureSet: FeatureSet = {
|
||||
if (!productConfig) {
|
||||
return NotFoundUtil.renderNotFound(res);
|
||||
}
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
`/compare/${req.params["product"]}`,
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/product-compare.ejs`, {
|
||||
support: false,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
@@ -1375,6 +1471,7 @@ const HomeFeatureSet: FeatureSet = {
|
||||
requestDemoCta: false,
|
||||
productConfig,
|
||||
onlyShowCompareTable: false,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
533
Home/Utils/PageSEO.ts
Normal file
533
Home/Utils/PageSEO.ts
Normal file
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* Page-specific SEO metadata configuration for OneUptime landing pages
|
||||
* This provides structured data for search engines and AI agents
|
||||
*/
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PageSEOData {
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalPath: string; // e.g., "/product/monitoring" - will be combined with homeUrl
|
||||
ogImage?: string; // defaults to /img/og-image.png if not specified
|
||||
ogType?: string; // defaults to "website"
|
||||
twitterCard?: "summary" | "summary_large_image"; // defaults to "summary_large_image" for product pages
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
// For SoftwareApplication schema on product pages
|
||||
softwareApplication?: {
|
||||
name: string;
|
||||
applicationCategory: string;
|
||||
operatingSystem: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
};
|
||||
// Page type for conditional schema rendering
|
||||
pageType:
|
||||
| "home"
|
||||
| "product"
|
||||
| "pricing"
|
||||
| "legal"
|
||||
| "blog"
|
||||
| "about"
|
||||
| "support"
|
||||
| "enterprise"
|
||||
| "compare"
|
||||
| "other";
|
||||
}
|
||||
|
||||
// Default SEO data factory
|
||||
export const createDefaultSEO: (
|
||||
title: string,
|
||||
description: string,
|
||||
canonicalPath: string,
|
||||
pageType?: PageSEOData["pageType"],
|
||||
) => PageSEOData = (
|
||||
title: string,
|
||||
description: string,
|
||||
canonicalPath: string,
|
||||
pageType: PageSEOData["pageType"] = "other",
|
||||
): PageSEOData => {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
canonicalPath,
|
||||
pageType,
|
||||
breadcrumbs: [{ name: "Home", url: "/" }],
|
||||
};
|
||||
};
|
||||
|
||||
// Page-specific SEO configurations
|
||||
export const PageSEOConfig: Record<string, PageSEOData> = {
|
||||
// Homepage
|
||||
"/": {
|
||||
title: "OneUptime | Complete Monitoring & Observability Platform",
|
||||
description:
|
||||
"OneUptime is an open-source complete observability platform. Monitor websites, APIs, and servers. Get alerts, manage incidents, and keep customers informed with status pages. Free tier available.",
|
||||
canonicalPath: "/",
|
||||
ogType: "website",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "home",
|
||||
breadcrumbs: [{ name: "Home", url: "/" }],
|
||||
},
|
||||
|
||||
// Product Pages
|
||||
"/product/status-page": {
|
||||
title: "Status Page | Free Public & Private Status Pages | OneUptime",
|
||||
description:
|
||||
"Create unlimited public and private status pages. Keep customers informed about incidents and scheduled maintenance. Custom branding, unlimited subscribers, SSL included. Open source.",
|
||||
canonicalPath: "/product/status-page",
|
||||
ogImage: "/img/status-pages.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "Status Page", url: "/product/status-page" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Status Page",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Create public and private status pages to communicate service health to customers and stakeholders.",
|
||||
features: [
|
||||
"Unlimited public and private status pages",
|
||||
"Custom branding and domain",
|
||||
"Unlimited subscribers",
|
||||
"Email, SMS, and webhook notifications",
|
||||
"Scheduled maintenance announcements",
|
||||
"Incident timeline and postmortems",
|
||||
"SSL certificates included",
|
||||
"API access",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/monitoring": {
|
||||
title: "Uptime Monitoring | Website, API, Server Monitoring | OneUptime",
|
||||
description:
|
||||
"Monitor websites, APIs, servers, and any resource in real-time. Get instant alerts when things go wrong. Supports HTTP, TCP, UDP, DNS, SSL, ping monitoring. Open source.",
|
||||
canonicalPath: "/product/monitoring",
|
||||
ogImage: "/img/monitor.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "Monitoring", url: "/product/monitoring" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Monitoring",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Real-time uptime monitoring for websites, APIs, servers, and infrastructure.",
|
||||
features: [
|
||||
"Website and API monitoring",
|
||||
"Server and infrastructure monitoring",
|
||||
"Synthetic monitoring with Playwright",
|
||||
"Custom monitoring criteria",
|
||||
"Multi-location checks",
|
||||
"1-second monitoring intervals",
|
||||
"SSL certificate monitoring",
|
||||
"Response time tracking",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/incident-management": {
|
||||
title:
|
||||
"Incident Management Software | Resolve Incidents Faster | OneUptime",
|
||||
description:
|
||||
"Streamline incident response with OneUptime. Track incidents, collaborate in real-time, conduct postmortems, and improve MTTR. Integrates with Slack, PagerDuty, and more. Open source.",
|
||||
canonicalPath: "/product/incident-management",
|
||||
ogImage: "/img/incident-report.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "Incident Management", url: "/product/incident-management" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Incident Management",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Complete incident management platform to detect, respond, and resolve incidents faster.",
|
||||
features: [
|
||||
"Incident tracking and timeline",
|
||||
"Real-time collaboration",
|
||||
"Postmortem reports",
|
||||
"Custom incident states and severity",
|
||||
"Slack and Teams integration",
|
||||
"Automated incident workflows",
|
||||
"MTTR and incident metrics",
|
||||
"Root cause analysis",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/on-call": {
|
||||
title:
|
||||
"On-Call Management & Alerting | Schedules & Escalations | OneUptime",
|
||||
description:
|
||||
"On-call scheduling, alerting, and escalation policies. Alert the right people at the right time via SMS, phone, email, Slack. Rotation schedules and override support. Open source.",
|
||||
canonicalPath: "/product/on-call",
|
||||
ogImage: "/img/on-call.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "On-Call & Alerts", url: "/product/on-call" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime On-Call Management",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud, iOS, Android",
|
||||
description:
|
||||
"On-call scheduling, alerting, and escalation management for DevOps and SRE teams.",
|
||||
features: [
|
||||
"On-call schedules and rotations",
|
||||
"Escalation policies",
|
||||
"SMS, phone, and email alerts",
|
||||
"Slack and Teams notifications",
|
||||
"Override and swap shifts",
|
||||
"Mobile app notifications",
|
||||
"Alert deduplication",
|
||||
"On-call reports",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/logs-management": {
|
||||
title: "Log Management | Fast Log Search & Analysis | OneUptime",
|
||||
description:
|
||||
"Centralized log management with blazing fast search. Ingest logs from any source via OpenTelemetry, Fluentd, or API. Set up alerts on log patterns. Open source.",
|
||||
canonicalPath: "/product/logs-management",
|
||||
ogImage: "/img/logs.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "Logs Management", url: "/product/logs-management" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Logs Management",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Centralized log management and analysis with fast search and alerting.",
|
||||
features: [
|
||||
"OpenTelemetry log ingestion",
|
||||
"Fluentd and Fluent Bit support",
|
||||
"Full-text log search",
|
||||
"Log pattern detection",
|
||||
"Log-based alerting",
|
||||
"Application and container logs",
|
||||
"Custom retention policies",
|
||||
"1000+ source integrations",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/apm": {
|
||||
title: "APM | Application Performance Monitoring | OneUptime",
|
||||
description:
|
||||
"Monitor application performance with distributed tracing, metrics, and error tracking. OpenTelemetry native. Track latency, throughput, and errors across services. Open source.",
|
||||
canonicalPath: "/product/apm",
|
||||
ogImage: "/img/apm.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "APM", url: "/product/apm" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime APM",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Application performance monitoring with distributed tracing, metrics, and error tracking.",
|
||||
features: [
|
||||
"Distributed tracing",
|
||||
"Performance metrics",
|
||||
"Error tracking and exceptions",
|
||||
"Service dependency maps",
|
||||
"OpenTelemetry native",
|
||||
"Latency analysis",
|
||||
"Throughput monitoring",
|
||||
"Custom dashboards",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/workflows": {
|
||||
title: "Workflow Automation | No-Code Integrations | OneUptime",
|
||||
description:
|
||||
"Build automated workflows without code. Connect 5000+ services, automate incident response, and create custom integrations. Trigger on any event. Open source.",
|
||||
canonicalPath: "/product/workflows",
|
||||
ogImage: "/img/workflows.png",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "Workflows", url: "/product/workflows" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime Workflows",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"No-code workflow automation for incident response and integrations.",
|
||||
features: [
|
||||
"Visual workflow builder",
|
||||
"5000+ integrations",
|
||||
"Event-driven triggers",
|
||||
"Conditional logic",
|
||||
"Custom code blocks",
|
||||
"Webhook triggers",
|
||||
"Scheduled workflows",
|
||||
"Error handling and retries",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Pricing
|
||||
"/pricing": {
|
||||
title: "Pricing | Free Tier & Paid Plans | OneUptime",
|
||||
description:
|
||||
"OneUptime pricing starts free. Get status pages, monitoring, incident management, and more. Transparent pricing with no hidden fees. Enterprise plans available.",
|
||||
canonicalPath: "/pricing",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "pricing",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Pricing", url: "/pricing" },
|
||||
],
|
||||
},
|
||||
|
||||
// Enterprise
|
||||
"/enterprise/overview": {
|
||||
title: "Enterprise | Self-Hosted & Cloud | OneUptime",
|
||||
description:
|
||||
"OneUptime for enterprise. Self-hosted deployment, SSO/SAML, advanced security, SLA guarantees, dedicated support. SOC 2, HIPAA, GDPR compliant.",
|
||||
canonicalPath: "/enterprise/overview",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "enterprise",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Enterprise", url: "/enterprise/overview" },
|
||||
],
|
||||
},
|
||||
|
||||
"/enterprise/demo": {
|
||||
title: "Request Demo | See OneUptime in Action | OneUptime",
|
||||
description:
|
||||
"Schedule a personalized demo of OneUptime. See how our observability platform can help your team monitor, respond, and resolve issues faster.",
|
||||
canonicalPath: "/enterprise/demo",
|
||||
twitterCard: "summary",
|
||||
pageType: "enterprise",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Enterprise", url: "/enterprise/overview" },
|
||||
{ name: "Request Demo", url: "/enterprise/demo" },
|
||||
],
|
||||
},
|
||||
|
||||
// About & Support
|
||||
"/about": {
|
||||
title: "About Us | Open Source Observability | OneUptime",
|
||||
description:
|
||||
"Learn about OneUptime, the open-source observability platform. Built by engineers, for engineers. Meet our contributors and learn our mission.",
|
||||
canonicalPath: "/about",
|
||||
twitterCard: "summary",
|
||||
pageType: "about",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "About", url: "/about" },
|
||||
],
|
||||
},
|
||||
|
||||
"/support": {
|
||||
title: "Support | Help & Documentation | OneUptime",
|
||||
description:
|
||||
"Get help with OneUptime. Access documentation, community support, and contact our team. Enterprise customers get priority support.",
|
||||
canonicalPath: "/support",
|
||||
twitterCard: "summary",
|
||||
pageType: "support",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Support", url: "/support" },
|
||||
],
|
||||
},
|
||||
|
||||
"/oss-friends": {
|
||||
title: "OSS Friends | Open Source Partners | OneUptime",
|
||||
description:
|
||||
"Meet our open source friends and partners. OneUptime is proud to be part of the open source community.",
|
||||
canonicalPath: "/oss-friends",
|
||||
twitterCard: "summary",
|
||||
pageType: "other",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "OSS Friends", url: "/oss-friends" },
|
||||
],
|
||||
},
|
||||
|
||||
// Legal pages
|
||||
"/legal": {
|
||||
title: "Legal Center | Terms, Privacy, Compliance | OneUptime",
|
||||
description:
|
||||
"OneUptime legal documents including terms of service, privacy policy, GDPR, SOC 2, HIPAA compliance information.",
|
||||
canonicalPath: "/legal",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
],
|
||||
},
|
||||
|
||||
"/legal/terms": {
|
||||
title: "Terms of Service | OneUptime",
|
||||
description: "OneUptime terms of service and conditions of use.",
|
||||
canonicalPath: "/legal/terms",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: "Terms of Service", url: "/legal/terms" },
|
||||
],
|
||||
},
|
||||
|
||||
"/legal/privacy": {
|
||||
title: "Privacy Policy | OneUptime",
|
||||
description:
|
||||
"OneUptime privacy policy. Learn how we collect, use, and protect your data.",
|
||||
canonicalPath: "/legal/privacy",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: "Privacy Policy", url: "/legal/privacy" },
|
||||
],
|
||||
},
|
||||
|
||||
"/legal/gdpr": {
|
||||
title: "GDPR Compliance | OneUptime",
|
||||
description:
|
||||
"OneUptime GDPR compliance information. We are committed to protecting EU citizen data rights.",
|
||||
canonicalPath: "/legal/gdpr",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: "GDPR", url: "/legal/gdpr" },
|
||||
],
|
||||
},
|
||||
|
||||
"/legal/soc-2": {
|
||||
title: "SOC 2 Compliance | OneUptime",
|
||||
description:
|
||||
"OneUptime SOC 2 Type II compliance. Our security controls are audited annually.",
|
||||
canonicalPath: "/legal/soc-2",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: "SOC 2", url: "/legal/soc-2" },
|
||||
],
|
||||
},
|
||||
|
||||
"/legal/hipaa": {
|
||||
title: "HIPAA Compliance | OneUptime",
|
||||
description:
|
||||
"OneUptime HIPAA compliance for healthcare organizations. We sign BAAs for enterprise customers.",
|
||||
canonicalPath: "/legal/hipaa",
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: "HIPAA", url: "/legal/hipaa" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to get SEO data for a path, with fallback
|
||||
export const getPageSEO: (path: string) => PageSEOData = (
|
||||
path: string,
|
||||
): PageSEOData => {
|
||||
// Exact match first
|
||||
if (PageSEOConfig[path]) {
|
||||
return PageSEOConfig[path];
|
||||
}
|
||||
|
||||
// For compare pages, create dynamic SEO
|
||||
if (path.startsWith("/compare/")) {
|
||||
const product: string = path.replace("/compare/", "");
|
||||
const productName: string = product
|
||||
.split("-")
|
||||
.map((word: string) => {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(" ");
|
||||
return {
|
||||
title: `OneUptime vs ${productName} | Comparison | OneUptime`,
|
||||
description: `Compare OneUptime with ${productName}. See features, pricing, and why teams choose OneUptime as their observability platform.`,
|
||||
canonicalPath: path,
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "compare",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Compare", url: "/#compare" },
|
||||
{ name: `vs ${productName}`, url: path },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// For legal subpages not explicitly defined
|
||||
if (path.startsWith("/legal/")) {
|
||||
const section: string = path.replace("/legal/", "");
|
||||
const sectionName: string = section
|
||||
.split("-")
|
||||
.map((word: string) => {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
})
|
||||
.join(" ");
|
||||
return {
|
||||
title: `${sectionName} | Legal | OneUptime`,
|
||||
description: `OneUptime ${sectionName.toLowerCase()} legal information and compliance documentation.`,
|
||||
canonicalPath: path,
|
||||
twitterCard: "summary",
|
||||
pageType: "legal",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{ name: sectionName, url: path },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return createDefaultSEO(
|
||||
"OneUptime | Complete Observability Platform",
|
||||
"OneUptime monitors websites, APIs, and servers and alerts your team if something goes wrong. It also keeps your customers updated about any downtime.",
|
||||
path,
|
||||
"other",
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSEOConfig;
|
||||
@@ -115,7 +115,7 @@ const products: Dictionary<Product> = {
|
||||
data: [
|
||||
{
|
||||
title: "Monitor anything",
|
||||
description: "Server, Containers, API's, Websites, IoT and more.",
|
||||
description: "Server, Containers, APIs, Websites, IoT and more.",
|
||||
productColumn: "",
|
||||
oneuptimeColumn: "tick",
|
||||
},
|
||||
@@ -268,7 +268,7 @@ const products: Dictionary<Product> = {
|
||||
data: [
|
||||
{
|
||||
title: "Monitor anything",
|
||||
description: "Server, Containers, API's, Websites, IoT and more.",
|
||||
description: "Server, Containers, APIs, Websites, IoT and more.",
|
||||
productColumn: "",
|
||||
oneuptimeColumn: "tick",
|
||||
},
|
||||
@@ -422,7 +422,7 @@ const products: Dictionary<Product> = {
|
||||
data: [
|
||||
{
|
||||
title: "Monitor anything",
|
||||
description: "Server, Containers, API's, Websites, IoT and more.",
|
||||
description: "Server, Containers, APIs, Websites, IoT and more.",
|
||||
productColumn: "Monitors only API and Websites.",
|
||||
oneuptimeColumn: "tick",
|
||||
},
|
||||
@@ -572,7 +572,7 @@ const products: Dictionary<Product> = {
|
||||
data: [
|
||||
{
|
||||
title: "Monitor anything",
|
||||
description: "Server, Containers, API's, Websites, IoT and more.",
|
||||
description: "Server, Containers, APIs, Websites, IoT and more.",
|
||||
productColumn: "",
|
||||
oneuptimeColumn: "tick",
|
||||
},
|
||||
|
||||
@@ -11,6 +11,90 @@ interface CachedSitemap {
|
||||
generatedAt: number; // epoch ms
|
||||
}
|
||||
|
||||
// Priority and changefreq configuration for different page types
|
||||
type ChangeFrequency =
|
||||
| "always"
|
||||
| "hourly"
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "yearly"
|
||||
| "never";
|
||||
|
||||
interface SitemapPageConfig {
|
||||
priority: number;
|
||||
changefreq: ChangeFrequency;
|
||||
}
|
||||
|
||||
const PAGE_CONFIG: Record<string, SitemapPageConfig> = {
|
||||
// Homepage - highest priority
|
||||
"/": { priority: 1.0, changefreq: "daily" },
|
||||
|
||||
// Core product pages - high priority
|
||||
"/product/status-page": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/monitoring": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/incident-management": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/on-call": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/logs-management": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/apm": { priority: 0.9, changefreq: "weekly" },
|
||||
"/product/workflows": { priority: 0.9, changefreq: "weekly" },
|
||||
|
||||
// Important pages
|
||||
"/pricing": { priority: 0.9, changefreq: "weekly" },
|
||||
"/enterprise/demo": { priority: 0.9, changefreq: "weekly" },
|
||||
"/enterprise/overview": { priority: 0.8, changefreq: "weekly" },
|
||||
"/about": { priority: 0.7, changefreq: "weekly" },
|
||||
"/support": { priority: 0.7, changefreq: "weekly" },
|
||||
|
||||
// Documentation and reference
|
||||
"/docs": { priority: 0.7, changefreq: "weekly" },
|
||||
"/reference": { priority: 0.7, changefreq: "weekly" },
|
||||
|
||||
// Blog section
|
||||
"/blog": { priority: 0.7, changefreq: "daily" },
|
||||
|
||||
// Community and legal
|
||||
"/oss-friends": { priority: 0.3, changefreq: "monthly" },
|
||||
};
|
||||
|
||||
// Default config for pages not explicitly listed
|
||||
const DEFAULT_CONFIG: SitemapPageConfig = {
|
||||
priority: 0.5,
|
||||
changefreq: "monthly",
|
||||
};
|
||||
|
||||
// Blog post config
|
||||
const BLOG_POST_CONFIG: SitemapPageConfig = {
|
||||
priority: 0.6,
|
||||
changefreq: "monthly",
|
||||
};
|
||||
|
||||
// Blog tag config
|
||||
const BLOG_TAG_CONFIG: SitemapPageConfig = {
|
||||
priority: 0.4,
|
||||
changefreq: "weekly",
|
||||
};
|
||||
|
||||
// Compare page config
|
||||
const COMPARE_PAGE_CONFIG: SitemapPageConfig = {
|
||||
priority: 0.7,
|
||||
changefreq: "monthly",
|
||||
};
|
||||
|
||||
function getPageConfig(path: string): SitemapPageConfig {
|
||||
// Check for exact match first
|
||||
if (PAGE_CONFIG[path]) {
|
||||
return PAGE_CONFIG[path];
|
||||
}
|
||||
|
||||
// Check for prefix matches
|
||||
if (path.startsWith("/legal")) {
|
||||
return { priority: 0.3, changefreq: "monthly" };
|
||||
}
|
||||
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
// 10 minutes TTL
|
||||
const TTL_MS: number = 10 * 60 * 1000;
|
||||
let cache: CachedSitemap | null = null;
|
||||
@@ -102,7 +186,7 @@ export const generateSitemapXml: () => Promise<string> =
|
||||
// Blog posts
|
||||
const blogPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList();
|
||||
const blogPostEntries: any[] = blogPosts.map((post: BlogPostHeader) => {
|
||||
const blogPostEntries: Entry[] = blogPosts.map((post: BlogPostHeader) => {
|
||||
// post.blogUrl already contains /blog/post/<slug>/view relative or absolute? In BlogPostUtil it's relative (starts with /blog...), so ensure absolute.
|
||||
const loc: string = post.blogUrl.startsWith("http")
|
||||
? post.blogUrl
|
||||
@@ -110,16 +194,20 @@ export const generateSitemapXml: () => Promise<string> =
|
||||
return {
|
||||
loc,
|
||||
lastmod: new Date(post.postDate).toISOString(),
|
||||
priority: BLOG_POST_CONFIG.priority,
|
||||
changefreq: BLOG_POST_CONFIG.changefreq,
|
||||
};
|
||||
});
|
||||
|
||||
// Blog tags
|
||||
const tags: string[] = await BlogPostUtil.getTags();
|
||||
const tagEntries: any[] = tags.map((tag: string) => {
|
||||
const tagEntries: Entry[] = tags.map((tag: string) => {
|
||||
const tagSlug: string = tag.toLowerCase().replace(/\s+/g, "-").trim();
|
||||
return {
|
||||
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
|
||||
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
|
||||
priority: BLOG_TAG_CONFIG.priority,
|
||||
changefreq: BLOG_TAG_CONFIG.changefreq,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -128,18 +216,25 @@ export const generateSitemapXml: () => Promise<string> =
|
||||
interface Entry {
|
||||
loc: string;
|
||||
lastmod: string;
|
||||
priority: number;
|
||||
changefreq: ChangeFrequency;
|
||||
}
|
||||
const entries: Entry[] = [
|
||||
...staticPaths.map((p: string) => {
|
||||
const config: SitemapPageConfig = getPageConfig(p);
|
||||
return {
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
priority: config.priority,
|
||||
changefreq: config.changefreq,
|
||||
};
|
||||
}),
|
||||
...productComparePaths.map((p: string) => {
|
||||
return {
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
priority: COMPARE_PAGE_CONFIG.priority,
|
||||
changefreq: COMPARE_PAGE_CONFIG.changefreq,
|
||||
};
|
||||
}),
|
||||
...blogPostEntries,
|
||||
@@ -174,6 +269,8 @@ export const generateSitemapXml: () => Promise<string> =
|
||||
const urlEle: XMLBuilder = urlset.ele("url");
|
||||
urlEle.ele("loc").txt(entry.loc);
|
||||
urlEle.ele("lastmod").txt(entry.lastmod);
|
||||
urlEle.ele("changefreq").txt(entry.changefreq);
|
||||
urlEle.ele("priority").txt(entry.priority.toFixed(1));
|
||||
}
|
||||
|
||||
const xml: string = urlset.end({ prettyPrint: true });
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="py-16">
|
||||
<div class="mx-auto max-w-8xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900">OneUptime is open source observability platform.</h1>
|
||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900">OneUptime is an open source observability platform.</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">Monitor, Observe, Debug, Resolve. Everything you need to build reliable software in one open source platform. Get started today.</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="/accounts/register" class="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Get started</a>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user