mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 08:42:13 +02:00
Compare commits
118 Commits
copilot-pl
...
probe-prox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d708fbbb52 | ||
|
|
03bceb959e | ||
|
|
efa411206e | ||
|
|
27fd99f2e8 | ||
|
|
07361bfeb7 | ||
|
|
bc8a5be0fa | ||
|
|
518768078a | ||
|
|
86e95f99ff | ||
|
|
ea48f56097 | ||
|
|
b8b9dd859a | ||
|
|
d28c14ef24 | ||
|
|
670bec2a12 | ||
|
|
aff24845a8 | ||
|
|
f280e97c1b | ||
|
|
62facf62dd | ||
|
|
db0387d81a | ||
|
|
5c4b19ab3d | ||
|
|
463755fa4d | ||
|
|
85888572de | ||
|
|
475bb25b2d | ||
|
|
badd200aed | ||
|
|
b40d87cbc9 | ||
|
|
36d0066b3a | ||
|
|
a49a0b2cba | ||
|
|
bada97d474 | ||
|
|
a1699f2d55 | ||
|
|
a11e054291 | ||
|
|
47cf7ba763 | ||
|
|
4e0dfb3664 | ||
|
|
250cb9e547 | ||
|
|
541257e3c6 | ||
|
|
ed43686736 | ||
|
|
9ca45f23e3 | ||
|
|
e3573a9b77 | ||
|
|
c9e78044e6 | ||
|
|
813581dec5 | ||
|
|
e528decf73 | ||
|
|
42ef41ede8 | ||
|
|
af26472db4 | ||
|
|
44b5c8b668 | ||
|
|
d821b88ed7 | ||
|
|
1df43e21ff | ||
|
|
76ca6ee7e1 | ||
|
|
dac731a57b | ||
|
|
0f4b248598 | ||
|
|
b2c14e0380 | ||
|
|
3ab9705bbe | ||
|
|
40812c8749 | ||
|
|
45ae1501f2 | ||
|
|
13d9f19606 | ||
|
|
ad3221310a | ||
|
|
659042fcfb | ||
|
|
d65b9c7b29 | ||
|
|
dc77206e6f | ||
|
|
9c1910d3f1 | ||
|
|
afe8f8e6f4 | ||
|
|
015bd0f870 | ||
|
|
383c145186 | ||
|
|
f155795e6b | ||
|
|
757f5b5721 | ||
|
|
694215df06 | ||
|
|
0eb6022f1d | ||
|
|
3109006828 | ||
|
|
272695bd11 | ||
|
|
330e3bc106 | ||
|
|
c7876bf3a3 | ||
|
|
345ada5404 | ||
|
|
4f97b1b460 | ||
|
|
e35ef1809f | ||
|
|
c2926f3542 | ||
|
|
9495b4bd47 | ||
|
|
ad3f36fdf5 | ||
|
|
f2221b0a40 | ||
|
|
62fbc1f4be | ||
|
|
054a2bc8f5 | ||
|
|
896787109c | ||
|
|
3a55fcc872 | ||
|
|
2945a48d05 | ||
|
|
da3a7ddb2e | ||
|
|
04a0bfedaa | ||
|
|
fa5c7b1e73 | ||
|
|
a1c2918cd7 | ||
|
|
91b11b12c1 | ||
|
|
778a34d631 | ||
|
|
6dbd838ca4 | ||
|
|
e09634dc6f | ||
|
|
af60715de2 | ||
|
|
3b4c54876e | ||
|
|
e357100e46 | ||
|
|
7f3a50076d | ||
|
|
9d182b6d55 | ||
|
|
33fce0b53c | ||
|
|
3db8419349 | ||
|
|
dfc324b099 | ||
|
|
36521ef37c | ||
|
|
a6f336340e | ||
|
|
c36f782192 | ||
|
|
5219f1cfc0 | ||
|
|
7f84d50baa | ||
|
|
cd2ce3f1a8 | ||
|
|
01b0e01ca8 | ||
|
|
73dc6bb5db | ||
|
|
ab7fc1c244 | ||
|
|
3927bea29c | ||
|
|
6060d66c2b | ||
|
|
9f4869b05f | ||
|
|
17bdfee012 | ||
|
|
4988b9fc7a | ||
|
|
9edc6b9f18 | ||
|
|
525e19faa6 | ||
|
|
588e8976d2 | ||
|
|
d5e28e98fb | ||
|
|
0e84bc9c40 | ||
|
|
39e8b1da6b | ||
|
|
66c4badd94 | ||
|
|
a245fabc34 | ||
|
|
fa9fce2774 | ||
|
|
a256f4be54 |
126
.github/workflows/build.yml
vendored
126
.github/workflows/build.yml
vendored
@@ -23,7 +23,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Accounts/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Accounts/Dockerfile .
|
||||
|
||||
docker-build-isolated-vm:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -38,7 +42,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./IsolatedVM/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./IsolatedVM/Dockerfile .
|
||||
|
||||
docker-build-home:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -53,7 +61,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Home/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Home/Dockerfile .
|
||||
|
||||
docker-build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -68,7 +80,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Worker/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Worker/Dockerfile .
|
||||
|
||||
docker-build-workflow:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -83,7 +99,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Workflow/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Workflow/Dockerfile .
|
||||
|
||||
docker-build-api-reference:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -98,7 +118,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./APIReference/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./APIReference/Dockerfile .
|
||||
|
||||
docker-build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,7 +137,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Docs/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Docs/Dockerfile .
|
||||
|
||||
|
||||
docker-build-otel-collector:
|
||||
@@ -129,7 +157,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./OTelCollector/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./OTelCollector/Dockerfile .
|
||||
|
||||
docker-build-app:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -145,7 +177,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./App/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./App/Dockerfile .
|
||||
|
||||
|
||||
docker-build-copilot:
|
||||
@@ -161,7 +197,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Copilot/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Copilot/Dockerfile .
|
||||
|
||||
docker-build-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -177,7 +217,11 @@ jobs:
|
||||
|
||||
# build image for accounts service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./E2E/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./E2E/Dockerfile .
|
||||
|
||||
docker-build-admin-dashboard:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -192,7 +236,11 @@ jobs:
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./AdminDashboard/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./AdminDashboard/Dockerfile .
|
||||
|
||||
docker-build-dashboard:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -207,7 +255,11 @@ jobs:
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Dashboard/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Dashboard/Dockerfile .
|
||||
|
||||
docker-build-probe:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -222,7 +274,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./Probe/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./Probe/Dockerfile .
|
||||
|
||||
docker-build-probe-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -237,7 +293,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./ProbeIngest/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./ProbeIngest/Dockerfile .
|
||||
|
||||
docker-build-server-monitor-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -252,7 +312,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
|
||||
|
||||
docker-build-open-telemetry-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -267,7 +331,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
|
||||
|
||||
docker-build-incoming-request-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -282,7 +350,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
|
||||
|
||||
docker-build-fluent-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -297,7 +369,11 @@ jobs:
|
||||
|
||||
# build image probe api
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./FluentIngest/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./FluentIngest/Dockerfile .
|
||||
|
||||
docker-build-status-page:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -312,7 +388,11 @@ jobs:
|
||||
|
||||
# build image for home
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./StatusPage/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./StatusPage/Dockerfile .
|
||||
|
||||
docker-build-test-server:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -327,4 +407,8 @@ jobs:
|
||||
|
||||
# build image for mail service
|
||||
- name: build docker image
|
||||
run: sudo docker build -f ./TestServer/Dockerfile .
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: sudo docker build -f ./TestServer/Dockerfile .
|
||||
|
||||
168
.github/workflows/compile.yml
vendored
168
.github/workflows/compile.yml
vendored
@@ -20,7 +20,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Accounts && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Accounts
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Accounts && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-isolated-vm:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -32,7 +37,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd IsolatedVM && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile IsolatedVM
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd IsolatedVM && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-common:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -43,7 +53,12 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Common
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Common && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-app:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -55,7 +70,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd App && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile App
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd App && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-home:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -67,7 +87,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Home && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Home
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Home && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-worker:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -79,7 +104,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Worker && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Worker
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Worker && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-workflow:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -91,7 +121,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Workflow && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Workflow
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Workflow && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-api-reference:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -103,7 +138,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd APIReference && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile API Reference
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd APIReference && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-docs-reference:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -115,7 +155,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Docs && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Docs Reference
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Docs && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-copilot:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -127,7 +172,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Copilot && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Copilot
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Copilot && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-nginx:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -140,7 +190,12 @@ jobs:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
|
||||
- run: cd Nginx && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Nginx
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Nginx && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-infrastructure-agent:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -150,7 +205,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
# Setup Go
|
||||
- uses: actions/setup-go@v5
|
||||
- run: cd InfrastructureAgent && go build .
|
||||
- name: Compile Infrastructure Agent
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd InfrastructureAgent && go build .
|
||||
|
||||
|
||||
compile-admin-dashboard:
|
||||
@@ -164,7 +224,12 @@ jobs:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
|
||||
- run: cd AdminDashboard && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Admin Dashboard
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd AdminDashboard && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-dashboard:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -177,7 +242,12 @@ jobs:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
|
||||
- run: cd Dashboard && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Dashboard
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Dashboard && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-e2e:
|
||||
@@ -191,7 +261,12 @@ jobs:
|
||||
node-version: latest
|
||||
- run: sudo apt-get update
|
||||
- run: cd Common && npm install
|
||||
- run: cd E2E && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile E2E
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd E2E && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-probe:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -203,7 +278,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd Probe && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Probe
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd Probe && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-probe-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -215,7 +295,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd ProbeIngest && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Probe Ingest
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd ProbeIngest && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-server-monitor-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -227,7 +312,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Server Monitor Ingest
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-open-telemetry-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -239,7 +329,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Open Telemetry Ingest
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-incoming-request-ingest:
|
||||
@@ -252,7 +347,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Incoming Request Ingest
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-fluent-ingest:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -264,7 +364,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd FluentIngest && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Fluent Ingest
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd FluentIngest && npm install && npm run compile && npm run dep-check
|
||||
|
||||
|
||||
compile-status-page:
|
||||
@@ -278,7 +383,12 @@ jobs:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
|
||||
- run: cd StatusPage && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Status Page
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd StatusPage && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-test-server:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -290,7 +400,12 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd TestServer && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile Test Server
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd TestServer && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-mcp:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -302,4 +417,9 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd MCP && npm install && npm run compile && npm run dep-check
|
||||
- name: Compile MCP
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd MCP && npm install && npm run compile && npm run dep-check
|
||||
667
.github/workflows/release.yml
vendored
667
.github/workflows/release.yml
vendored
@@ -184,18 +184,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file ./MCP/Dockerfile.tpl \
|
||||
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag oneuptime/mcp-server:release \
|
||||
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag ghcr.io/oneuptime/mcp-server:release \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
|
||||
--push .
|
||||
echo "✅ Pushed Docker images to Docker Hub and GitHub Container Registry"
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file ./MCP/Dockerfile.tpl \
|
||||
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag oneuptime/mcp-server:release \
|
||||
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag ghcr.io/oneuptime/mcp-server:release \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
|
||||
--push .
|
||||
echo "✅ Pushed Docker images to Docker Hub and GitHub Container Registry"
|
||||
|
||||
- name: Upload MCP server artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -252,17 +256,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Nginx/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Nginx/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/nginx:release \
|
||||
--tag oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/nginx:release \
|
||||
--tag ghcr.io/oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
e2e-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -312,17 +321,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./E2E/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./E2E/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/e2e:release \
|
||||
--tag oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/e2e:release \
|
||||
--tag ghcr.io/oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
isolated-vm-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -372,17 +386,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./IsolatedVM/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./IsolatedVM/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/isolated-vm:release \
|
||||
--tag oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/isolated-vm:release \
|
||||
--tag ghcr.io/oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
home-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -432,17 +451,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Home/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Home/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/home:release \
|
||||
--tag oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/home:release \
|
||||
--tag ghcr.io/oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -495,17 +519,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./TestServer/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./TestServer/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/test-server:release \
|
||||
--tag oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/test-server:release \
|
||||
--tag ghcr.io/oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
otel-collector-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -555,17 +584,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./OTelCollector/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./OTelCollector/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/otel-collector:release \
|
||||
--tag oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/otel-collector:release \
|
||||
--tag ghcr.io/oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -617,17 +651,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./StatusPage/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./StatusPage/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/status-page:release \
|
||||
--tag oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/status-page:release \
|
||||
--tag ghcr.io/oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
test-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -677,17 +716,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Tests/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Tests/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/test:release \
|
||||
--tag oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/test:release \
|
||||
--tag ghcr.io/oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
probe-ingest-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -737,17 +781,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./ProbeIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./ProbeIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/probe-ingest:release \
|
||||
--tag oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/probe-ingest:release \
|
||||
--tag ghcr.io/oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
server-monitor-ingest-docker-image-deploy:
|
||||
@@ -798,17 +847,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./ServerMonitorIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./ServerMonitorIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/server-monitor-ingest:release \
|
||||
--tag oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/server-monitor-ingest:release \
|
||||
--tag ghcr.io/oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -860,17 +914,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./OpenTelemetryIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./OpenTelemetryIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/open-telemetry-ingest:release \
|
||||
--tag oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/open-telemetry-ingest:release \
|
||||
--tag ghcr.io/oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
incoming-request-ingest-docker-image-deploy:
|
||||
@@ -921,17 +980,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./IncomingRequestIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./IncomingRequestIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/incoming-request-ingest:release \
|
||||
--tag oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/incoming-request-ingest:release \
|
||||
--tag ghcr.io/oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
fluent-ingest-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -981,17 +1045,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./FluentIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./FluentIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/fluent-ingest:release \
|
||||
--tag oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/fluent-ingest:release \
|
||||
--tag ghcr.io/oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
probe-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -1041,17 +1110,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Probe/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Probe/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/probe:release \
|
||||
--tag oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/probe:release \
|
||||
--tag ghcr.io/oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
admin-dashboard-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -1101,17 +1175,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./AdminDashboard/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./AdminDashboard/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/admin-dashboard:release \
|
||||
--tag oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/admin-dashboard:release \
|
||||
--tag ghcr.io/oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
dashboard-docker-image-deploy:
|
||||
@@ -1162,17 +1241,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Dashboard/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Dashboard/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/dashboard:release \
|
||||
--tag oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/dashboard:release \
|
||||
--tag ghcr.io/oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
app-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -1222,17 +1306,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./App/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./App/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/app:release \
|
||||
--tag oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/app:release \
|
||||
--tag ghcr.io/oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
copilot-docker-image-deploy:
|
||||
@@ -1283,17 +1372,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Copilot/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Copilot/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/copilot:release \
|
||||
--tag oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/copilot:release \
|
||||
--tag ghcr.io/oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
accounts-docker-image-deploy:
|
||||
needs: [generate-build-number]
|
||||
@@ -1343,17 +1437,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Accounts/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Accounts/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/accounts:release \
|
||||
--tag oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/accounts:release \
|
||||
--tag ghcr.io/oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
publish-npm-packages:
|
||||
@@ -1443,17 +1542,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./LLM/Dockerfile
|
||||
context: ./LLM
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./LLM/Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--push \
|
||||
--tag oneuptime/llm:release \
|
||||
--tag oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/llm:release \
|
||||
--tag ghcr.io/oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
./LLM
|
||||
|
||||
docs-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1505,17 +1609,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Docs/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Docs/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/docs:release \
|
||||
--tag oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/docs:release \
|
||||
--tag ghcr.io/oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1570,17 +1679,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Worker/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Worker/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/worker:release \
|
||||
--tag oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/worker:release \
|
||||
--tag ghcr.io/oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1635,17 +1749,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Workflow/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Workflow/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/workflow:release \
|
||||
--tag oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/workflow:release \
|
||||
--tag ghcr.io/oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1761,17 +1880,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./APIReference/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 45
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./APIReference/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/api-reference:release \
|
||||
--tag oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--tag ghcr.io/oneuptime/api-reference:release \
|
||||
--tag ghcr.io/oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1898,12 +2022,49 @@ jobs:
|
||||
configuration: "./Scripts/Release/ChangelogConfig.json"
|
||||
- run: echo "Changelog:"
|
||||
- run: echo "${{steps.build_changelog.outputs.changelog}}"
|
||||
- name: Fallback to commit messages if changelog empty
|
||||
id: fallback_changelog
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHANGELOG_CONTENT="${{steps.build_changelog.outputs.changelog}}"
|
||||
OLD_PLACEHOLDER="No significant changes were made. We have just fixed minor bugs for this release. You can find the detailed information in the commit history."
|
||||
NEW_PLACEHOLDER="(auto) No categorized pull requests. Fallback will list raw commit messages."
|
||||
if echo "$CHANGELOG_CONTENT" | grep -Fq "$OLD_PLACEHOLDER" || echo "$CHANGELOG_CONTENT" | grep -Fq "$NEW_PLACEHOLDER"; then
|
||||
echo "Detected empty placeholder changelog. Building commit list fallback."
|
||||
# Find previous tag (skip the most recent tag which might be for an older release). If none, include all commits.
|
||||
if prev_tag=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null); then
|
||||
echo "Previous tag: $prev_tag"
|
||||
commits=$(git log --pretty=format:'- %s (%h)' "$prev_tag"..HEAD)
|
||||
else
|
||||
echo "No previous tag found; using full commit history on this branch."
|
||||
commits=$(git log --pretty=format:'- %s (%h)')
|
||||
fi
|
||||
# If still empty (e.g., no commits), keep placeholder to avoid empty body.
|
||||
if [ -z "$commits" ]; then
|
||||
commits="(no commits found)"
|
||||
fi
|
||||
{
|
||||
echo "changelog<<EOF"
|
||||
echo "## Commit Messages"
|
||||
echo ""
|
||||
echo "$commits"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Pass through original changelog
|
||||
{
|
||||
echo "changelog<<EOF"
|
||||
echo "$CHANGELOG_CONTENT"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: "7.0.${{needs.generate-build-number.outputs.build_number}}"
|
||||
artifactErrorsFailBuild: true
|
||||
body: |
|
||||
${{steps.build_changelog.outputs.changelog}}
|
||||
${{steps.fallback_changelog.outputs.changelog}}
|
||||
|
||||
|
||||
infrastructure-agent-deploy:
|
||||
|
||||
@@ -77,17 +77,21 @@ jobs:
|
||||
ls -la "$PROVIDER_DIR" || true
|
||||
|
||||
- name: Test Go build
|
||||
run: |
|
||||
PROVIDER_DIR="./Terraform"
|
||||
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
|
||||
cd "$PROVIDER_DIR"
|
||||
echo "🔨 Testing Go build..."
|
||||
go mod tidy
|
||||
go build -v ./...
|
||||
echo "✅ Go build successful"
|
||||
else
|
||||
echo "⚠️ Cannot test build - missing go.mod or provider directory"
|
||||
fi
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
PROVIDER_DIR="./Terraform"
|
||||
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
|
||||
cd "$PROVIDER_DIR"
|
||||
echo "🔨 Testing Go build..."
|
||||
go mod tidy
|
||||
go build -v ./...
|
||||
echo "✅ Go build successful"
|
||||
else
|
||||
echo "⚠️ Cannot test build - missing go.mod or provider directory"
|
||||
fi
|
||||
|
||||
- name: Upload Terraform provider as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
629
.github/workflows/test-release.yaml
vendored
629
.github/workflows/test-release.yaml
vendored
@@ -176,18 +176,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images (test)
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file ./MCP/Dockerfile.tpl \
|
||||
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag oneuptime/mcp-server:test \
|
||||
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag ghcr.io/oneuptime/mcp-server:test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
|
||||
--push .
|
||||
echo "✅ Pushed test Docker images to Docker Hub and GitHub Container Registry"
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file ./MCP/Dockerfile.tpl \
|
||||
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag oneuptime/mcp-server:test \
|
||||
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
|
||||
--tag ghcr.io/oneuptime/mcp-server:test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
|
||||
--push .
|
||||
echo "✅ Pushed test Docker images to Docker Hub and GitHub Container Registry"
|
||||
|
||||
- name: Upload MCP server artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -269,18 +273,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./LLM/Dockerfile
|
||||
context: ./LLM
|
||||
# arm64 is not supported by the base image of the LLM
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./LLM/Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--push \
|
||||
--tag oneuptime/llm:test \
|
||||
--tag oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/llm:test \
|
||||
--tag ghcr.io/oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
./LLM
|
||||
|
||||
|
||||
nginx-docker-image-deploy:
|
||||
@@ -332,17 +340,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Nginx/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Nginx/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/nginx:test \
|
||||
--tag oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/nginx:test \
|
||||
--tag ghcr.io/oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
e2e-docker-image-deploy:
|
||||
@@ -394,17 +407,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./E2E/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./E2E/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/e2e:test \
|
||||
--tag oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/e2e:test \
|
||||
--tag ghcr.io/oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
test-server-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -455,17 +473,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./TestServer/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./TestServer/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/test-server:test \
|
||||
--tag oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/test-server:test \
|
||||
--tag ghcr.io/oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
otel-collector-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -516,17 +539,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./OTelCollector/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./OTelCollector/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/otel-collector:test \
|
||||
--tag oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/otel-collector:test \
|
||||
--tag ghcr.io/oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
isolated-vm-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -577,17 +605,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./IsolatedVM/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./IsolatedVM/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/isolated-vm:test \
|
||||
--tag oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/isolated-vm:test \
|
||||
--tag ghcr.io/oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
home-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -638,17 +671,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Home/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Home/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/home:test \
|
||||
--tag oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/home:test \
|
||||
--tag ghcr.io/oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -701,17 +739,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./StatusPage/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./StatusPage/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/status-page:test \
|
||||
--tag oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/status-page:test \
|
||||
--tag ghcr.io/oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -764,17 +807,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Tests/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Tests/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/test:test \
|
||||
--tag oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/test:test \
|
||||
--tag ghcr.io/oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
probe-ingest-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -825,17 +873,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./ProbeIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./ProbeIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/probe-ingest:test \
|
||||
--tag oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/probe-ingest:test \
|
||||
--tag ghcr.io/oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -888,17 +941,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./ServerMonitorIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./ServerMonitorIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/server-monitor-ingest:test \
|
||||
--tag oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/server-monitor-ingest:test \
|
||||
--tag ghcr.io/oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -952,17 +1010,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./IncomingRequestIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./IncomingRequestIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/incoming-request-ingest:test \
|
||||
--tag oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/incoming-request-ingest:test \
|
||||
--tag ghcr.io/oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
open-telemetry-ingest-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1013,17 +1076,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./OpenTelemetryIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./OpenTelemetryIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/open-telemetry-ingest:test \
|
||||
--tag oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/open-telemetry-ingest:test \
|
||||
--tag ghcr.io/oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
fluent-ingest-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1074,17 +1142,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./FluentIngest/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./FluentIngest/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/fluent-ingest:test \
|
||||
--tag oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/fluent-ingest:test \
|
||||
--tag ghcr.io/oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
probe-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1135,17 +1208,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Probe/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Probe/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/probe:test \
|
||||
--tag oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/probe:test \
|
||||
--tag ghcr.io/oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
dashboard-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1196,17 +1274,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Dashboard/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Dashboard/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/dashboard:test \
|
||||
--tag oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/dashboard:test \
|
||||
--tag ghcr.io/oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
admin-dashboard-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1257,17 +1340,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./AdminDashboard/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./AdminDashboard/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/admin-dashboard:test \
|
||||
--tag oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/admin-dashboard:test \
|
||||
--tag ghcr.io/oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
app-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1318,17 +1406,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./App/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./App/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/app:test \
|
||||
--tag oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/app:test \
|
||||
--tag ghcr.io/oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1382,17 +1475,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./APIReference/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./APIReference/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/api-reference:test \
|
||||
--tag oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/api-reference:test \
|
||||
--tag ghcr.io/oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
@@ -1445,17 +1543,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Accounts/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Accounts/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/accounts:test \
|
||||
--tag oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/accounts:test \
|
||||
--tag ghcr.io/oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
worker-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1506,17 +1609,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Worker/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.copilot-docker-image-deploybuild_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Worker/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/worker:test \
|
||||
--tag oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/worker:test \
|
||||
--tag ghcr.io/oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
copilot-docker-image-deploy:
|
||||
needs: generate-build-number
|
||||
@@ -1567,17 +1675,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Copilot/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Copilot/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/copilot:test \
|
||||
--tag oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/copilot:test \
|
||||
--tag ghcr.io/oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
workflow-docker-image-deploy:
|
||||
@@ -1629,17 +1742,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Workflow/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Workflow/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/workflow:test \
|
||||
--tag oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/workflow:test \
|
||||
--tag ghcr.io/oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
docs-docker-image-deploy:
|
||||
@@ -1691,17 +1809,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
file: ./Docs/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
GIT_SHA=${{ github.sha }}
|
||||
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: |
|
||||
docker buildx build \
|
||||
--file ./Docs/Dockerfile \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
--tag oneuptime/docs:test \
|
||||
--tag oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--tag ghcr.io/oneuptime/docs:test \
|
||||
--tag ghcr.io/oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
|
||||
.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
"ignore": [
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -135,7 +135,6 @@
|
||||
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
|
||||
<link rel="icon" href="/img/ou-wb.svg">
|
||||
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
|
||||
<link rel="canonical" href="/">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
|
||||
<meta property="og:url" content="https://oneuptime.com">
|
||||
|
||||
@@ -538,11 +538,6 @@ import WorkspaceSettingService, {
|
||||
Service as WorkspaceSettingServiceType,
|
||||
} from "Common/Server/Services/WorkspaceSettingService";
|
||||
|
||||
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
|
||||
import ProjectUserService, {
|
||||
Service as ProjectUserServiceType,
|
||||
} from "Common/Server/Services/ProjectUserService";
|
||||
|
||||
import MonitorFeed from "Common/Models/DatabaseModels/MonitorFeed";
|
||||
import MonitorFeedService, {
|
||||
Service as MonitorFeedServiceType,
|
||||
@@ -736,14 +731,6 @@ const BaseAPIFeatureSet: FeatureSet = {
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
new BaseAPI<ProjectUser, ProjectUserServiceType>(
|
||||
ProjectUser,
|
||||
ProjectUserService,
|
||||
).getRouter(),
|
||||
);
|
||||
|
||||
//service provider setting
|
||||
app.use(
|
||||
`/${APP_NAME.toLocaleLowerCase()}`,
|
||||
|
||||
@@ -20,7 +20,6 @@ import NotFoundException from "Common/Types/Exception/NotFoundException";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
|
||||
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
import {
|
||||
@@ -89,6 +88,8 @@ const handleUserTeamOperations: (
|
||||
ignoreHooks: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`SCIM Team operations - user added to team: ${team.id}`);
|
||||
} else {
|
||||
logger.debug(
|
||||
`SCIM Team operations - user already member of team: ${team.id}`,
|
||||
@@ -168,7 +169,7 @@ router.get(
|
||||
);
|
||||
|
||||
// Build query for team members in this project
|
||||
const query: Query<ProjectUser> = {
|
||||
const query: Query<TeamMember> = {
|
||||
projectId: projectId,
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
|
||||
<link rel="icon" href="/img/ou-wb.svg">
|
||||
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
|
||||
<link rel="canonical" href="/">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
|
||||
<meta property="og:url" content="https://oneuptime.com">
|
||||
|
||||
@@ -4,5 +4,13 @@
|
||||
"ignore": [
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {
|
||||
"useFsEvents": false,
|
||||
"interval": 500
|
||||
},
|
||||
"env": {
|
||||
"TS_NODE_TRANSPILE_ONLY": "1",
|
||||
"TS_NODE_FILES": "false"
|
||||
},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -178,7 +178,7 @@ import WorkspaceUserAuthToken from "./WorkspaceUserAuthToken";
|
||||
import WorkspaceProjectAuthToken from "./WorkspaceProjectAuthToken";
|
||||
import WorkspaceSetting from "./WorkspaceSetting";
|
||||
import WorkspaceNotificationRule from "./WorkspaceNotificationRule";
|
||||
import ProjectUser from "./ProjectUser";
|
||||
|
||||
import OnCallDutyPolicyUserOverride from "./OnCallDutyPolicyUserOverride";
|
||||
import MonitorFeed from "./MonitorFeed";
|
||||
import MetricType from "./MetricType";
|
||||
@@ -380,8 +380,6 @@ const AllModelTypes: Array<{
|
||||
WorkspaceSetting,
|
||||
WorkspaceNotificationRule,
|
||||
|
||||
ProjectUser,
|
||||
|
||||
MonitorFeed,
|
||||
|
||||
MetricType,
|
||||
|
||||
@@ -248,6 +248,84 @@ export default class Project extends TenantModel {
|
||||
})
|
||||
public paymentProviderCustomerId?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProject,
|
||||
Permission.UnAuthorizedSsoUser,
|
||||
Permission.ProjectUser,
|
||||
],
|
||||
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.LongText,
|
||||
title: "Business Details / Billing Address",
|
||||
description:
|
||||
"Business legal name, address and any tax information to appear on invoices.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public businessDetails?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProject,
|
||||
Permission.UnAuthorizedSsoUser,
|
||||
Permission.ProjectUser,
|
||||
],
|
||||
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Business Country (ISO Alpha-2)",
|
||||
description:
|
||||
"Two-letter ISO country code for billing address (e.g., US, GB, DE).",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public businessDetailsCountry?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProject,
|
||||
Permission.UnAuthorizedSsoUser,
|
||||
Permission.ProjectUser,
|
||||
],
|
||||
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Email,
|
||||
title: "Finance / Accounting Email",
|
||||
description:
|
||||
"Invoices, receipts and billing related notifications will be sent to this email in addition to project owner.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Email,
|
||||
length: ColumnLength.Email,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public financeAccountingEmail?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
import Project from "./Project";
|
||||
import Team from "./Team";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import AllowUserQueryWithoutTenant from "../../Types/Database/AllowUserQueryWithoutTenant";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
||||
import MultiTenentQueryAllowed from "../../Types/Database/MultiTenentQueryAllowed";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@MultiTenentQueryAllowed(true)
|
||||
@AllowUserQueryWithoutTenant(true)
|
||||
@CurrentUserCanAccessRecordBy("userId")
|
||||
@TenantColumn("projectId")
|
||||
@CrudApiEndpoint(new Route("/project-user"))
|
||||
@Entity({
|
||||
name: "ProjectUser",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "ProjectUser",
|
||||
singularName: "Project User",
|
||||
pluralName: "Project Users",
|
||||
icon: IconProp.User,
|
||||
tableDescription:
|
||||
"This model connects users and teams. This is an internal table. Its a view on TeamMembers table.",
|
||||
})
|
||||
export default class ProjectUser extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: Team,
|
||||
title: "Teams",
|
||||
description: "Teams to which this user belongs.",
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return Team;
|
||||
},
|
||||
{ eager: false },
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ProjectUserAcceptedTeams",
|
||||
inverseJoinColumn: {
|
||||
name: "teamId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
joinColumn: {
|
||||
name: "projectUserId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public acceptedTeams?: Array<Team> = undefined; // user is accepted to these teams. This is a view on TeamMembers table.
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: Team,
|
||||
title: "Teams",
|
||||
description: "Teams to which this user belongs.",
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return Team;
|
||||
},
|
||||
{ eager: false },
|
||||
)
|
||||
@JoinTable({
|
||||
name: "ProjectUserInvitedTeams",
|
||||
inverseJoinColumn: {
|
||||
name: "teamId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
joinColumn: {
|
||||
name: "projectUserId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public invitedTeams?: Array<Team> = undefined; // user is invited to these teams.
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.ProjectMember,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User who belongs to this team.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectTeam,
|
||||
Permission.CurrentUser,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
title: "User ID",
|
||||
description: "ID of User who belongs to this team",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
}
|
||||
@@ -1179,6 +1179,45 @@ export default class StatusPage extends BaseModel {
|
||||
})
|
||||
public enableSlackSubscribers?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateProjectStatusPage,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadProjectStatusPage,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditProjectStatusPage,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Enable Microsoft Teams Subscribers",
|
||||
description:
|
||||
"Can Microsoft Teams subscribers subscribe to this Status Page?",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
@ColumnBillingAccessControl({
|
||||
read: PlanType.Free,
|
||||
update: PlanType.Scale,
|
||||
create: PlanType.Free,
|
||||
})
|
||||
public enableMicrosoftTeamsSubscribers?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -379,6 +379,65 @@ export default class StatusPageSubscriber extends BaseModel {
|
||||
})
|
||||
public slackWorkspaceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateStatusPageSubscriber,
|
||||
Permission.Public,
|
||||
],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongURL,
|
||||
title: "Microsoft Teams Incoming Webhook URL",
|
||||
description:
|
||||
"Microsoft Teams incoming webhook URL to send notifications to Teams channel",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongURL,
|
||||
transformer: URL.getDatabaseTransformer(),
|
||||
})
|
||||
public microsoftTeamsIncomingWebhookUrl?: URL = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateStatusPageSubscriber,
|
||||
Permission.Public,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadStatusPageSubscriber,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditStatusPageSubscriber,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Microsoft Teams Workspace Name",
|
||||
description:
|
||||
"Name of the Microsoft Teams workspace for validation and identification",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public microsoftTeamsWorkspaceName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
|
||||
@@ -502,6 +502,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
footerHTML: true,
|
||||
enableEmailSubscribers: true,
|
||||
enableSlackSubscribers: true,
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
enableSmsSubscribers: true,
|
||||
isPublicStatusPage: true,
|
||||
allowSubscribersToChooseResources: true,
|
||||
@@ -2146,6 +2147,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
projectId: true,
|
||||
enableEmailSubscribers: true,
|
||||
enableSlackSubscribers: true,
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
enableSmsSubscribers: true,
|
||||
allowSubscribersToChooseResources: true,
|
||||
allowSubscribersToChooseEventTypes: true,
|
||||
@@ -2419,6 +2421,7 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
enableEmailSubscribers: true,
|
||||
enableSmsSubscribers: true,
|
||||
enableSlackSubscribers: true,
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
allowSubscribersToChooseResources: true,
|
||||
allowSubscribersToChooseEventTypes: true,
|
||||
showSubscriberPageOnStatusPage: true,
|
||||
@@ -2480,15 +2483,28 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
}
|
||||
|
||||
if (
|
||||
!req.body.data["subscriberEmail"] &&
|
||||
!req.body.data["subscriberPhone"] &&
|
||||
!req.body.data["slackWorkspaceName"]
|
||||
req.body.data["microsoftTeamsWorkspaceName"] &&
|
||||
!statusPage.enableMicrosoftTeamsSubscribers
|
||||
) {
|
||||
logger.debug(
|
||||
`No email, phone, or slack workspace name provided for subscription to status page with ID: ${objectId}`,
|
||||
`Microsoft Teams subscribers not enabled for status page with ID: ${objectId}`,
|
||||
);
|
||||
throw new BadDataException(
|
||||
"Email, phone or slack workspace name is required to subscribe to this status page.",
|
||||
"Microsoft Teams subscribers not enabled for this status page.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!req.body.data["subscriberEmail"] &&
|
||||
!req.body.data["subscriberPhone"] &&
|
||||
!req.body.data["slackWorkspaceName"] &&
|
||||
!req.body.data["microsoftTeamsWorkspaceName"]
|
||||
) {
|
||||
logger.debug(
|
||||
`No email, phone, slack workspace name, or Microsoft Teams workspace name provided for subscription to status page with ID: ${objectId}`,
|
||||
);
|
||||
throw new BadDataException(
|
||||
"Email, phone, slack workspace name, or Microsoft Teams workspace name is required to subscribe to this status page.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2512,6 +2528,18 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
? (req.body.data["slackWorkspaceName"] as string)
|
||||
: undefined;
|
||||
|
||||
const microsoftTeamsIncomingWebhookUrl: string | undefined = req.body.data[
|
||||
"microsoftTeamsIncomingWebhookUrl"
|
||||
]
|
||||
? (req.body.data["microsoftTeamsIncomingWebhookUrl"] as string)
|
||||
: undefined;
|
||||
|
||||
const microsoftTeamsWorkspaceName: string | undefined = req.body.data[
|
||||
"microsoftTeamsWorkspaceName"
|
||||
]
|
||||
? (req.body.data["microsoftTeamsWorkspaceName"] as string)
|
||||
: undefined;
|
||||
|
||||
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
||||
|
||||
let isUpdate: boolean = false;
|
||||
@@ -2570,6 +2598,23 @@ export default class StatusPageAPI extends BaseAPI<
|
||||
statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
|
||||
}
|
||||
|
||||
if (microsoftTeamsIncomingWebhookUrl) {
|
||||
logger.debug(
|
||||
`Setting subscriber Microsoft Teams webhook: ${microsoftTeamsIncomingWebhookUrl}`,
|
||||
);
|
||||
statusPageSubscriber.microsoftTeamsIncomingWebhookUrl = URL.fromString(
|
||||
microsoftTeamsIncomingWebhookUrl,
|
||||
);
|
||||
}
|
||||
|
||||
if (microsoftTeamsWorkspaceName) {
|
||||
logger.debug(
|
||||
`Setting subscriber Microsoft Teams workspace name: ${microsoftTeamsWorkspaceName}`,
|
||||
);
|
||||
statusPageSubscriber.microsoftTeamsWorkspaceName =
|
||||
microsoftTeamsWorkspaceName;
|
||||
}
|
||||
|
||||
if (
|
||||
req.body.data["statusPageResources"] &&
|
||||
!statusPage.allowSubscribersToChooseResources
|
||||
|
||||
@@ -68,6 +68,28 @@ export class MigrationName1754671483948 implements MigrationInterface {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageAnnouncement" ADD "subscriberNotificationStatusMessage" text`,
|
||||
);
|
||||
// Set all existing rows' subscriber notification statuses to 'Success' since they were previously considered notified
|
||||
await queryRunner.query(
|
||||
`UPDATE "Incident" SET "subscriberNotificationStatusOnIncidentCreated"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "IncidentPublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "IncidentStateTimeline" SET "subscriberNotificationStatus"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "ScheduledMaintenance" SET "subscriberNotificationStatusOnEventScheduled"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "ScheduledMaintenancePublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "ScheduledMaintenanceStateTimeline" SET "subscriberNotificationStatus"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "StatusPageAnnouncement" SET "subscriberNotificationStatus"='Success'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1755775040650 implements MigrationInterface {
|
||||
public name = "MigrationName1755775040650";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" ADD "enableMicrosoftTeamsSubscribers" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsIncomingWebhookUrl" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" character varying(100)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsIncomingWebhookUrl"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPage" DROP COLUMN "enableMicrosoftTeamsSubscribers"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1755778495455 implements MigrationInterface {
|
||||
public name = "MigrationName1755778495455";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" character varying(100)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1755778934927 implements MigrationInterface {
|
||||
public name = "MigrationName1755778934927";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsIncomingWebhookUrl"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsIncomingWebhookUrl" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1756293325324 implements MigrationInterface {
|
||||
public name = "MigrationName1756293325324";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "businessDetails" character varying(500)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "businessDetails"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1756296282627 implements MigrationInterface {
|
||||
public name = "MigrationName1756296282627";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "businessDetailsCountry" character varying(100)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "businessDetailsCountry"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1756300358095 implements MigrationInterface {
|
||||
public name = "MigrationName1756300358095";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "financeAccountingEmail" character varying(100)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "financeAccountingEmail"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,12 @@ import { MigrationName1755088852971 } from "./1755088852971-MigrationName";
|
||||
import { MigrationName1755093133870 } from "./1755093133870-MigrationName";
|
||||
import { MigrationName1755109893911 } from "./1755109893911-MigrationName";
|
||||
import { MigrationName1755110936888 } from "./1755110936888-MigrationName";
|
||||
import { MigrationName1755775040650 } from "./1755775040650-MigrationName";
|
||||
import { MigrationName1755778495455 } from "./1755778495455-MigrationName";
|
||||
import { MigrationName1755778934927 } from "./1755778934927-MigrationName";
|
||||
import { MigrationName1756293325324 } from "./1756293325324-MigrationName";
|
||||
import { MigrationName1756296282627 } from "./1756296282627-MigrationName";
|
||||
import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -320,4 +326,10 @@ export default [
|
||||
MigrationName1755093133870,
|
||||
MigrationName1755109893911,
|
||||
MigrationName1755110936888,
|
||||
MigrationName1755775040650,
|
||||
MigrationName1755778495455,
|
||||
MigrationName1755778934927,
|
||||
MigrationName1756293325324,
|
||||
MigrationName1756296282627,
|
||||
MigrationName1756300358095,
|
||||
];
|
||||
|
||||
@@ -14,7 +14,20 @@ export default class QueueWorker {
|
||||
public static getWorker(
|
||||
queueName: QueueName,
|
||||
onJobInQueue: (job: QueueJob) => Promise<void>,
|
||||
options: { concurrency: number },
|
||||
options: {
|
||||
concurrency: number;
|
||||
/**
|
||||
* How long (in ms) the worker will hold a lock on the job before it's considered stalled
|
||||
* if the event loop is blocked and the lock cannot be extended in time.
|
||||
* Defaults to BullMQ default (30s) if not provided.
|
||||
*/
|
||||
lockDuration?: number;
|
||||
/**
|
||||
* Maximum number of times a job can be re-processed due to stall detection
|
||||
* before being moved to failed. Defaults to BullMQ default (1) if not provided.
|
||||
*/
|
||||
maxStalledCount?: number;
|
||||
},
|
||||
): Worker {
|
||||
const worker: Worker = new Worker(queueName, onJobInQueue, {
|
||||
connection: {
|
||||
@@ -23,6 +36,11 @@ export default class QueueWorker {
|
||||
password: RedisPassword,
|
||||
},
|
||||
concurrency: options.concurrency,
|
||||
// Only set these values if provided so we do not override BullMQ defaults
|
||||
...(options.lockDuration ? { lockDuration: options.lockDuration } : {}),
|
||||
...(options.maxStalledCount !== undefined
|
||||
? { maxStalledCount: options.maxStalledCount }
|
||||
: {}),
|
||||
});
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
|
||||
@@ -80,6 +80,94 @@ export class BillingService extends BaseService {
|
||||
await this.stripe.customers.update(id, { name: newName });
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async updateCustomerBusinessDetails(
|
||||
id: string,
|
||||
businessDetails: string,
|
||||
countryCode?: string | null,
|
||||
financeAccountingEmail?: string | null,
|
||||
): Promise<void> {
|
||||
if (!this.isBillingEnabled()) {
|
||||
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
|
||||
}
|
||||
// Goal: Update Stripe Customer "Billing details" (address fields) rather than invoice footer.
|
||||
// We only have a single free-form textarea. We'll map:
|
||||
// First non-empty line -> address.line1
|
||||
// Second non-empty line (if any) and remaining (joined, truncated) -> address.line2
|
||||
// We also persist full text in metadata so we can reconstruct or improve parsing later.
|
||||
// NOTE: Because Stripe requires structured address, any city/state/postal/country detection
|
||||
// would be heuristic; we keep it simple unless we later add structured fields.
|
||||
|
||||
const lines: Array<string> = businessDetails
|
||||
.split(/\r?\n/)
|
||||
.map((l: string) => {
|
||||
return l.trim();
|
||||
})
|
||||
.filter((l: string) => {
|
||||
return l.length > 0;
|
||||
});
|
||||
|
||||
let line1: string | undefined = undefined;
|
||||
let line2: string | undefined = undefined;
|
||||
|
||||
if (lines && lines.length > 0) {
|
||||
const first: string = lines[0]!; // non-null
|
||||
line1 = first.substring(0, 200); // Stripe typical limit safeguard.
|
||||
}
|
||||
if (lines && lines.length > 1) {
|
||||
const rest: string = lines.slice(1).join(", ");
|
||||
line2 = rest.substring(0, 200);
|
||||
}
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
business_details_full: businessDetails.substring(0, 5000),
|
||||
};
|
||||
if (financeAccountingEmail) {
|
||||
metadata["finance_accounting_email"] = financeAccountingEmail.substring(
|
||||
0,
|
||||
200,
|
||||
);
|
||||
} else {
|
||||
// Remove if cleared
|
||||
metadata["finance_accounting_email"] = "";
|
||||
}
|
||||
|
||||
const updateParams: Stripe.CustomerUpdateParams = {
|
||||
metadata,
|
||||
address: {},
|
||||
};
|
||||
|
||||
// If finance / accounting email provided, set it as the customer email so Stripe sends
|
||||
// invoices / receipts there. (Stripe only supports a single email via API currently.)
|
||||
if (financeAccountingEmail && financeAccountingEmail.trim().length > 0) {
|
||||
updateParams.email = financeAccountingEmail.trim();
|
||||
}
|
||||
|
||||
if (line1) {
|
||||
updateParams.address = updateParams.address || {};
|
||||
updateParams.address.line1 = line1;
|
||||
}
|
||||
if (line2) {
|
||||
updateParams.address = updateParams.address || {};
|
||||
updateParams.address.line2 = line2;
|
||||
}
|
||||
if (countryCode) {
|
||||
updateParams.address = updateParams.address || {};
|
||||
// Stripe expects uppercase 2-letter ISO code
|
||||
updateParams.address.country = countryCode.toUpperCase();
|
||||
}
|
||||
|
||||
if (!line1 && !line2 && !countryCode) {
|
||||
// Clear address if empty details submitted.
|
||||
updateParams.address = {
|
||||
line1: "",
|
||||
line2: "",
|
||||
} as any;
|
||||
}
|
||||
|
||||
await this.stripe.customers.update(id, updateParams);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async deleteCustomer(id: string): Promise<void> {
|
||||
if (!this.isBillingEnabled()) {
|
||||
|
||||
@@ -67,6 +67,7 @@ import QueryOperator from "../../Types/BaseDatabase/QueryOperator";
|
||||
import { FindWhere } from "../../Types/BaseDatabase/Query";
|
||||
import logger from "../Utils/Logger";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -162,11 +163,11 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
|
||||
if (!monitor) {
|
||||
throw new BadDataException("Monitor not found.");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
if (!monitor.id) {
|
||||
throw new BadDataException("Monitor id not found.");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
projectId = monitor.projectId!;
|
||||
@@ -1389,7 +1390,7 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
});
|
||||
|
||||
if (!monitor) {
|
||||
throw new BadDataException("Monitor not found.");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
return (monitor.postUpdatesToWorkspaceChannels || []).filter(
|
||||
@@ -1419,7 +1420,7 @@ ${createdItem.description?.trim() || "No description provided."}
|
||||
});
|
||||
|
||||
if (!monitor) {
|
||||
throw new BadDataException("Monitor not found.");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
return monitor.name || "";
|
||||
|
||||
@@ -128,18 +128,50 @@ export default class OTelIngestService {
|
||||
Metric,
|
||||
) as Metric;
|
||||
|
||||
// Handle start timestamp safely
|
||||
if (datapoint["startTimeUnixNano"]) {
|
||||
newDbMetric.startTimeUnixNano = datapoint["startTimeUnixNano"] as number;
|
||||
newDbMetric.startTime = OneUptimeDate.fromUnixNano(
|
||||
datapoint["startTimeUnixNano"] as number,
|
||||
);
|
||||
try {
|
||||
let startTimeUnixNano: number;
|
||||
if (typeof datapoint["startTimeUnixNano"] === "string") {
|
||||
startTimeUnixNano = parseFloat(datapoint["startTimeUnixNano"]);
|
||||
if (isNaN(startTimeUnixNano)) {
|
||||
startTimeUnixNano = OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
}
|
||||
} else {
|
||||
startTimeUnixNano =
|
||||
(datapoint["startTimeUnixNano"] as number) ||
|
||||
OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
}
|
||||
newDbMetric.startTimeUnixNano = startTimeUnixNano;
|
||||
newDbMetric.startTime = OneUptimeDate.fromUnixNano(startTimeUnixNano);
|
||||
} catch {
|
||||
const currentNano: number = OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
newDbMetric.startTimeUnixNano = currentNano;
|
||||
newDbMetric.startTime = OneUptimeDate.getCurrentDate();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle end timestamp safely
|
||||
if (datapoint["timeUnixNano"]) {
|
||||
newDbMetric.timeUnixNano = datapoint["timeUnixNano"] as number;
|
||||
newDbMetric.time = OneUptimeDate.fromUnixNano(
|
||||
datapoint["timeUnixNano"] as number,
|
||||
);
|
||||
try {
|
||||
let timeUnixNano: number;
|
||||
if (typeof datapoint["timeUnixNano"] === "string") {
|
||||
timeUnixNano = parseFloat(datapoint["timeUnixNano"]);
|
||||
if (isNaN(timeUnixNano)) {
|
||||
timeUnixNano = OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
}
|
||||
} else {
|
||||
timeUnixNano =
|
||||
(datapoint["timeUnixNano"] as number) ||
|
||||
OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
}
|
||||
newDbMetric.timeUnixNano = timeUnixNano;
|
||||
newDbMetric.time = OneUptimeDate.fromUnixNano(timeUnixNano);
|
||||
} catch {
|
||||
const currentNano: number = OneUptimeDate.getCurrentDateAsUnixNano();
|
||||
newDbMetric.timeUnixNano = currentNano;
|
||||
newDbMetric.time = OneUptimeDate.getCurrentDate();
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(datapoint).includes("asInt")) {
|
||||
@@ -174,7 +206,7 @@ export default class OTelIngestService {
|
||||
serviceName: data.telemetryServiceName,
|
||||
}),
|
||||
...TelemetryUtil.getAttributes({
|
||||
items: datapoint["attributes"] as JSONArray,
|
||||
items: (datapoint["attributes"] as JSONArray) || [],
|
||||
prefixKeysWithString: "metricAttributes",
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -273,6 +273,38 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
updateBy: UpdateBy<Model>,
|
||||
): Promise<OnUpdate<Model>> {
|
||||
if (IsBillingEnabled) {
|
||||
if (
|
||||
updateBy.data.businessDetails ||
|
||||
updateBy.data.businessDetailsCountry ||
|
||||
updateBy.data.financeAccountingEmail
|
||||
) {
|
||||
// Sync to Stripe.
|
||||
const project: Model | null = await this.findOneById({
|
||||
id: new ObjectID(updateBy.query._id! as string),
|
||||
select: {
|
||||
paymentProviderCustomerId: true,
|
||||
financeAccountingEmail: true,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (project?.paymentProviderCustomerId) {
|
||||
try {
|
||||
await BillingService.updateCustomerBusinessDetails(
|
||||
project.paymentProviderCustomerId,
|
||||
(updateBy.data.businessDetails as string) || "",
|
||||
(updateBy.data.businessDetailsCountry as string) || null,
|
||||
(updateBy.data.financeAccountingEmail as string) ||
|
||||
(project as any).financeAccountingEmail ||
|
||||
null,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to update Stripe customer business details: " + err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
|
||||
await NotificationService.rechargeIfBalanceIsLow(
|
||||
new ObjectID(updateBy.query._id! as string),
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
||||
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/ProjectUser";
|
||||
import TeamMemberService from "./TeamMemberService";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async refreshProjectUsersByProject(data: {
|
||||
projectId: ObjectID;
|
||||
}): Promise<void> {
|
||||
// get all team members by user
|
||||
|
||||
// first delete all project users by project id.
|
||||
await this.deleteBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
// get all team members by project.
|
||||
const teamMembers: Array<TeamMember> = await TeamMemberService.findBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
teamId: true,
|
||||
team: {
|
||||
_id: true,
|
||||
},
|
||||
hasAcceptedInvitation: true,
|
||||
},
|
||||
});
|
||||
|
||||
// create project users by team members.
|
||||
|
||||
const projectUsersToCreate: Array<Model> = [];
|
||||
|
||||
for (const teamMember of teamMembers) {
|
||||
// check if the user already exists in the project users.
|
||||
|
||||
// if yes then add the team to the project user acceptedTeams, if the invitation is accepted.
|
||||
|
||||
// if no then create a new project user.
|
||||
|
||||
// if the user is not accepted the invitation then add the team to invitedTeams of the project user.
|
||||
|
||||
// if the user is accepted the invitation then add the team to acceptedTeams of the project user.
|
||||
|
||||
let doesProjectUserExist: boolean = false;
|
||||
|
||||
for (const item of projectUsersToCreate) {
|
||||
if (item.userId?.toString() === teamMember.userId?.toString()) {
|
||||
doesProjectUserExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (doesProjectUserExist) {
|
||||
// add the team to the project user acceptedTeams, if the invitation is accepted.
|
||||
if (teamMember.hasAcceptedInvitation) {
|
||||
for (const projectUser of projectUsersToCreate) {
|
||||
if (
|
||||
projectUser.userId?.toString() === teamMember.userId?.toString()
|
||||
) {
|
||||
if (!projectUser.acceptedTeams) {
|
||||
projectUser.acceptedTeams = [];
|
||||
}
|
||||
|
||||
projectUser.acceptedTeams?.push(teamMember.team!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const projectUser of projectUsersToCreate) {
|
||||
if (
|
||||
projectUser.userId?.toString() === teamMember.userId?.toString()
|
||||
) {
|
||||
if (!projectUser.invitedTeams) {
|
||||
projectUser.invitedTeams = [];
|
||||
}
|
||||
|
||||
projectUser.invitedTeams?.push(teamMember.team!);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// create a new project user.
|
||||
const projectUser: Model = new Model();
|
||||
projectUser.userId = teamMember.userId!;
|
||||
projectUser.projectId = data.projectId;
|
||||
|
||||
if (teamMember.hasAcceptedInvitation) {
|
||||
projectUser.acceptedTeams = [teamMember.team!];
|
||||
} else {
|
||||
projectUser.invitedTeams = [teamMember.team!];
|
||||
}
|
||||
|
||||
projectUsersToCreate.push(projectUser);
|
||||
}
|
||||
}
|
||||
|
||||
// now create the project users.
|
||||
|
||||
for (const projectUser of projectUsersToCreate) {
|
||||
await this.create({
|
||||
data: projectUser,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -340,10 +340,26 @@ export class Service extends DatabaseService<StatusPage> {
|
||||
projectId: ObjectID,
|
||||
statusPageId: ObjectID,
|
||||
): Promise<URL> {
|
||||
const dahboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
||||
if (!projectId) {
|
||||
throw new BadDataException(
|
||||
"projectId is required to build status page dashboard link",
|
||||
);
|
||||
}
|
||||
|
||||
return URL.fromString(dahboardUrl.toString()).addRoute(
|
||||
`/${projectId.toString()}/status-pages/${statusPageId.toString()}`,
|
||||
if (!statusPageId) {
|
||||
throw new BadDataException(
|
||||
"statusPageId is required to build status page dashboard link",
|
||||
);
|
||||
}
|
||||
|
||||
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
||||
|
||||
// Defensive: ensure objects have toString
|
||||
const projectIdStr: string = projectId.toString();
|
||||
const statusPageIdStr: string = statusPageId.toString();
|
||||
|
||||
return URL.fromString(dashboardUrl.toString()).addRoute(
|
||||
`/${projectIdStr}/status-pages/${statusPageIdStr}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
|
||||
import NumberUtil from "../../Utils/Number";
|
||||
import SlackUtil from "../Utils/Workspace/Slack/Slack";
|
||||
import MicrosoftTeamsUtil from "../Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
@@ -221,6 +222,23 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Microsoft Teams webhook URL if provided
|
||||
if (data.data.microsoftTeamsIncomingWebhookUrl) {
|
||||
logger.debug(
|
||||
`Microsoft Teams Incoming Webhook URL: ${data.data.microsoftTeamsIncomingWebhookUrl}`,
|
||||
);
|
||||
if (
|
||||
!MicrosoftTeamsUtil.isValidMicrosoftTeamsIncomingWebhookUrl(
|
||||
data.data.microsoftTeamsIncomingWebhookUrl,
|
||||
)
|
||||
) {
|
||||
logger.debug("Invalid Microsoft Teams Incoming Webhook URL.");
|
||||
throw new BadDataException(
|
||||
"Invalid Microsoft Teams Incoming Webhook URL.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(
|
||||
100000,
|
||||
999999,
|
||||
@@ -370,16 +388,55 @@ Stay informed about service availability! 🚀`;
|
||||
|
||||
logger.debug(`Slack Message: ${slackMessage}`);
|
||||
|
||||
try {
|
||||
await SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(createdItem.slackIncomingWebhookUrl.toString()),
|
||||
text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
|
||||
SlackUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(createdItem.slackIncomingWebhookUrl.toString()),
|
||||
text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug("Slack notification sent successfully.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.error("Error sending Slack notification:");
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
// if Microsoft Teams incoming webhook is provided and sendYouHaveSubscribedMessage is true, then send a message to the Teams channel.
|
||||
if (
|
||||
createdItem.microsoftTeamsIncomingWebhookUrl &&
|
||||
createdItem.sendYouHaveSubscribedMessage
|
||||
) {
|
||||
logger.debug("Sending Microsoft Teams notification for new subscriber.");
|
||||
const teamsMessage: string = `## 📢 New Subscription to ${statusPageName}
|
||||
|
||||
**You have successfully subscribed to receive status updates!**
|
||||
|
||||
🔗 **Status Page:** [${statusPageName}](${statusPageURL})
|
||||
📧 **Manage Subscription:** [Update preferences or unsubscribe](${unsubscribeLink})
|
||||
|
||||
You will receive real-time notifications for:
|
||||
• Incidents and outages
|
||||
• Scheduled maintenance events
|
||||
• Service announcements
|
||||
• Status updates
|
||||
|
||||
Stay informed about service availability! 🚀`;
|
||||
|
||||
logger.debug(`Teams Message: ${teamsMessage}`);
|
||||
|
||||
MicrosoftTeamsUtil.sendMessageToChannelViaIncomingWebhook({
|
||||
url: URL.fromString(
|
||||
createdItem.microsoftTeamsIncomingWebhookUrl.toString(),
|
||||
),
|
||||
text: teamsMessage,
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug("Microsoft Teams notification sent successfully.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.error("Error sending Microsoft Teams notification:");
|
||||
logger.error(err);
|
||||
});
|
||||
logger.debug("Slack notification sent successfully.");
|
||||
} catch (error) {
|
||||
logger.error("Error sending Slack notification:");
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("onCreateSuccess completed.");
|
||||
@@ -691,6 +748,7 @@ Stay informed about service availability! 🚀`;
|
||||
subscriberPhone: true,
|
||||
subscriberWebhook: true,
|
||||
slackIncomingWebhookUrl: true,
|
||||
microsoftTeamsIncomingWebhookUrl: true,
|
||||
isSubscribedToAllResources: true,
|
||||
statusPageResources: true,
|
||||
isSubscribedToAllEventTypes: true,
|
||||
|
||||
@@ -33,7 +33,6 @@ import PositiveNumber from "../../Types/PositiveNumber";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import TeamMember from "../../Models/DatabaseModels/TeamMember";
|
||||
import User from "../../Models/DatabaseModels/User";
|
||||
import ProjectUserService from "./ProjectUserService";
|
||||
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
|
||||
@@ -198,12 +197,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
onCreate.createBy.data.projectId!,
|
||||
);
|
||||
|
||||
ProjectUserService.refreshProjectUsersByProject({
|
||||
projectId: onCreate.createBy.data.projectId!,
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
|
||||
return createdItem;
|
||||
}
|
||||
|
||||
@@ -247,12 +240,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
item.user?.email as Email,
|
||||
);
|
||||
}
|
||||
|
||||
ProjectUserService.refreshProjectUsersByProject({
|
||||
projectId: item.projectId!,
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
return { updateBy, carryForward: onUpdate.carryForward };
|
||||
@@ -335,13 +322,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
||||
item.userId!,
|
||||
item.projectId!,
|
||||
);
|
||||
|
||||
// refresh project users.
|
||||
ProjectUserService.refreshProjectUsersByProject({
|
||||
projectId: item.projectId!,
|
||||
}).catch((err: Error) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
return onDelete;
|
||||
|
||||
@@ -49,6 +49,7 @@ import WorkspaceNotificationLog from "../../Models/DatabaseModels/WorkspaceNotif
|
||||
import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
|
||||
import WorkspaceNotificationStatus from "../../Types/Workspace/WorkspaceNotificationStatus";
|
||||
import WorkspaceNotificationActionType from "../../Types/Workspace/WorkspaceNotificationActionType";
|
||||
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
|
||||
|
||||
export interface MessageBlocksByWorkspaceType {
|
||||
workspaceType: WorkspaceType;
|
||||
@@ -1911,7 +1912,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
|
||||
if (!monitor) {
|
||||
logger.debug("Monitor not found for ID:");
|
||||
logger.debug(data.notificationFor.monitorId);
|
||||
throw new BadDataException("Monitor ID not found");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
const monitorLabels: Array<Label> = monitor?.labels || [];
|
||||
|
||||
@@ -19,9 +19,8 @@ export default class Markdown {
|
||||
markdown: string,
|
||||
contentType: MarkdownContentType,
|
||||
): Promise<string> {
|
||||
// convert tags > and < to > and <
|
||||
markdown = markdown.replace(/</g, "<");
|
||||
markdown = markdown.replace(/>/g, ">");
|
||||
// Basic sanitization: neutralize script tags but preserve markdown syntax like '>' for blockquotes.
|
||||
markdown = markdown.replace(/<script/gi, "<script");
|
||||
|
||||
let renderer: Renderer | null = null;
|
||||
|
||||
@@ -78,7 +77,7 @@ export default class Markdown {
|
||||
};
|
||||
|
||||
renderer.code = function (code, language) {
|
||||
return `<pre class="language-${language} rounded-md"><code class="language-${language} rounded-md">${code}</code></pre>`;
|
||||
return `<pre class="language-${language} rounded-xl bg-slate-900/95 text-slate-100 p-5 overflow-x-auto text-sm shadow-md ring-1 ring-slate-900/10"><code class="language-${language}">${code}</code></pre>`;
|
||||
};
|
||||
|
||||
renderer.heading = function (text, level) {
|
||||
@@ -137,6 +136,102 @@ export default class Markdown {
|
||||
return `<h6 class="my-5 tracking-tight font-bold text-gray-800">${text}</h6>`;
|
||||
};
|
||||
|
||||
// Lists
|
||||
renderer.list = function (body, ordered, start) {
|
||||
const tag: string = ordered ? "ol" : "ul";
|
||||
const cls: string = ordered
|
||||
? "list-decimal pl-6 my-6 space-y-2 text-gray-700"
|
||||
: "list-disc pl-6 my-6 space-y-2 text-gray-700";
|
||||
const startAttr: string =
|
||||
ordered && start !== 1 ? ` start="${start}"` : "";
|
||||
return `<${tag}${startAttr} class="${cls}">${body}</${tag}>`;
|
||||
};
|
||||
renderer.listitem = function (text) {
|
||||
return `<li class="leading-7">${text}</li>`;
|
||||
};
|
||||
|
||||
// Tables
|
||||
renderer.table = function (header, body) {
|
||||
return `<div class="overflow-x-auto my-8"><table class="min-w-full border border-gray-200 text-sm text-left">
|
||||
${header}${body}
|
||||
</table></div>`;
|
||||
};
|
||||
renderer.tablerow = function (content) {
|
||||
return `<tr class="border-b last:border-b-0">${content}</tr>`;
|
||||
};
|
||||
renderer.tablecell = function (content, flags) {
|
||||
const type: string = flags.header ? "th" : "td";
|
||||
const base: string = "px-4 py-2 border-r last:border-r-0 border-gray-200";
|
||||
const align: string = flags.align ? ` text-${flags.align}` : "";
|
||||
const weight: string = flags.header ? " font-semibold bg-gray-50" : "";
|
||||
return `<${type} class="${base}${align}${weight}">${content}</${type}>`;
|
||||
};
|
||||
|
||||
// Inline code
|
||||
renderer.codespan = function (code) {
|
||||
return `<code class="rounded-md bg-gray-100 px-1.5 py-0.5 text-sm text-pink-600">${code}</code>`;
|
||||
};
|
||||
|
||||
// Horizontal rule
|
||||
renderer.hr = function () {
|
||||
return '<hr class="my-12 border-t border-gray-200" />';
|
||||
};
|
||||
|
||||
// Emphasis / Strong / Strikethrough
|
||||
renderer.strong = function (text) {
|
||||
return `<strong class="font-semibold text-gray-800">${text}</strong>`;
|
||||
};
|
||||
renderer.em = function (text) {
|
||||
return `<em class="italic text-gray-700">${text}</em>`;
|
||||
};
|
||||
renderer.del = function (text) {
|
||||
return `<del class="line-through text-gray-400">${text}</del>`;
|
||||
};
|
||||
|
||||
// Images
|
||||
renderer.image = function (href, _title, text) {
|
||||
return `<figure class="my-8"><img src="${href}" alt="${text}" class="rounded-xl shadow-sm border border-gray-200" loading="lazy"/><figcaption class="mt-2 text-center text-sm text-gray-500">${text || ""}</figcaption></figure>`;
|
||||
};
|
||||
|
||||
// Links
|
||||
// We explicitly add underline + color classes because Tailwind Typography (prose-*)
|
||||
// styles may get overridden by surrounding utility classes or global resets.
|
||||
// External links open in a new tab with proper rel attributes; internal links stay in-page.
|
||||
renderer.link = function (href, title, text) {
|
||||
// Guard: if no href, just return the text.
|
||||
if (!href) {
|
||||
return text as string;
|
||||
}
|
||||
|
||||
const isHash: boolean = href.startsWith("#");
|
||||
const isMailTo: boolean = href.startsWith("mailto:");
|
||||
const isTel: boolean = href.startsWith("tel:");
|
||||
const isInternal: boolean =
|
||||
href.startsWith("/") ||
|
||||
href.includes("oneuptime.com") ||
|
||||
isHash ||
|
||||
isMailTo ||
|
||||
isTel;
|
||||
|
||||
const baseClasses: string = [
|
||||
"font-semibold",
|
||||
"text-indigo-600",
|
||||
"underline",
|
||||
"underline-offset-2",
|
||||
"decoration-indigo-300",
|
||||
"hover:decoration-indigo-500",
|
||||
"hover:text-indigo-500",
|
||||
"transition-colors",
|
||||
].join(" ");
|
||||
|
||||
const titleAttr: string = title ? ` title="${title}"` : "";
|
||||
const externalAttrs: string = isInternal
|
||||
? ""
|
||||
: ' target="_blank" rel="noopener noreferrer"';
|
||||
|
||||
return `<a href="${href}"${titleAttr} class="${baseClasses}"${externalAttrs}>${text}</a>`;
|
||||
};
|
||||
|
||||
this.blogRenderer = renderer;
|
||||
|
||||
return renderer;
|
||||
|
||||
@@ -58,6 +58,7 @@ import CaptureSpan from "../Telemetry/CaptureSpan";
|
||||
import MetricType from "../../../Models/DatabaseModels/MetricType";
|
||||
import MonitorLog from "../../../Models/AnalyticsModels/MonitorLog";
|
||||
import MonitorLogService from "../../Services/MonitorLogService";
|
||||
import ExceptionMessages from "../../../Types/Exception/ExceptionMessages";
|
||||
|
||||
export default class MonitorResourceUtil {
|
||||
@CaptureSpan()
|
||||
@@ -109,7 +110,7 @@ export default class MonitorResourceUtil {
|
||||
|
||||
if (!monitor) {
|
||||
logger.debug(`${dataToProcess.monitorId.toString()} Monitor not found`);
|
||||
throw new BadDataException("Monitor not found");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
if (!monitor.projectId) {
|
||||
@@ -126,9 +127,7 @@ export default class MonitorResourceUtil {
|
||||
`${dataToProcess.monitorId.toString()} Monitor is disabled. Please enable it to start monitoring again.`,
|
||||
);
|
||||
|
||||
throw new BadDataException(
|
||||
"Monitor is disabled. Please enable it to start monitoring again.",
|
||||
);
|
||||
throw new BadDataException(ExceptionMessages.MonitorDisabled);
|
||||
}
|
||||
|
||||
if (monitor.disableActiveMonitoringBecauseOfManualIncident) {
|
||||
|
||||
@@ -3,414 +3,154 @@ import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
||||
import URL from "../../../../Types/API/URL";
|
||||
import { JSONObject } from "../../../../Types/JSON";
|
||||
import API from "../../../../Utils/API";
|
||||
import WorkspaceMessagePayload from "../../../../Types/Workspace/WorkspaceMessagePayload";
|
||||
import logger from "../../Logger";
|
||||
import Dictionary from "../../../../Types/Dictionary";
|
||||
import WorkspaceBase, {
|
||||
WorkspaceChannel,
|
||||
WorkspaceSendMessageResponse,
|
||||
WorkspaceThread,
|
||||
} from "../WorkspaceBase";
|
||||
import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
|
||||
import OneUptimeDate from "../../../../Types/Date";
|
||||
import WorkspaceBase from "../WorkspaceBase";
|
||||
import CaptureSpan from "../../Telemetry/CaptureSpan";
|
||||
import BadDataException from "../../../../Types/Exception/BadDataException";
|
||||
|
||||
export default class MicrosoftTeams extends WorkspaceBase {
|
||||
@CaptureSpan()
|
||||
public static override async getAllWorkspaceChannels(data: {
|
||||
authToken: string;
|
||||
}): Promise<Dictionary<WorkspaceChannel>> {
|
||||
logger.debug("Getting all workspace channels with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const channels: Dictionary<WorkspaceChannel> = {};
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get<JSONObject>(
|
||||
URL.fromString("https://graph.microsoft.com/v1.0/me/joinedTeams"),
|
||||
{
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
);
|
||||
|
||||
logger.debug("Response from Microsoft Graph API for getting all channels:");
|
||||
logger.debug(JSON.stringify(response, null, 2));
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error response from Microsoft Graph API:");
|
||||
logger.error(response);
|
||||
throw response;
|
||||
}
|
||||
|
||||
for (const team of (response.jsonData as JSONObject)?.[
|
||||
"value"
|
||||
] as Array<JSONObject>) {
|
||||
if (!team["id"] || !team["displayName"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
channels[team["displayName"].toString()] = {
|
||||
id: team["id"] as string,
|
||||
name: team["displayName"] as string,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug("All workspace channels obtained:");
|
||||
logger.debug(channels);
|
||||
return channels;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override getDividerBlock(): JSONObject {
|
||||
return {
|
||||
type: "divider",
|
||||
};
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static getValuesFromView(data: {
|
||||
view: JSONObject;
|
||||
}): Dictionary<string | number | Array<string | number> | Date> {
|
||||
logger.debug("Getting values from view with data:");
|
||||
logger.debug(JSON.stringify(data, null, 2));
|
||||
|
||||
const teamsView: JSONObject = data.view;
|
||||
const values: Dictionary<string | number | Array<string | number> | Date> =
|
||||
{};
|
||||
|
||||
if (!teamsView["state"] || !(teamsView["state"] as JSONObject)["values"]) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const valueId in (teamsView["state"] as JSONObject)[
|
||||
"values"
|
||||
] as JSONObject) {
|
||||
for (const blockId in (
|
||||
(teamsView["state"] as JSONObject)["values"] as JSONObject
|
||||
)[valueId] as JSONObject) {
|
||||
const valueObject: JSONObject = (
|
||||
(teamsView["state"] as JSONObject)["values"] as JSONObject
|
||||
)[valueId] as JSONObject;
|
||||
const value: JSONObject = valueObject[blockId] as JSONObject;
|
||||
values[blockId] = value["value"] as string | number;
|
||||
|
||||
if ((value["selected_option"] as JSONObject)?.["value"]) {
|
||||
values[blockId] = (value["selected_option"] as JSONObject)?.[
|
||||
"value"
|
||||
] as string;
|
||||
}
|
||||
|
||||
if (Array.isArray(value["selected_options"])) {
|
||||
values[blockId] = (
|
||||
value["selected_options"] as Array<JSONObject>
|
||||
).map((option: JSONObject) => {
|
||||
return option["value"] as string | number;
|
||||
});
|
||||
}
|
||||
|
||||
// if date picker
|
||||
if (value["selected_date_time"]) {
|
||||
values[blockId] = OneUptimeDate.fromUnixTimestamp(
|
||||
value["selected_date_time"] as number,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Values obtained from view:");
|
||||
logger.debug(values);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async inviteUserToChannelByChannelName(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
workspaceUserId: string;
|
||||
}): Promise<void> {
|
||||
logger.debug("Inviting user to channel with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const channelId: string = (
|
||||
await this.getWorkspaceChannelFromChannelName({
|
||||
authToken: data.authToken,
|
||||
channelName: data.channelName,
|
||||
private static buildMessageCardFromMarkdown(markdown: string): JSONObject {
|
||||
// Teams MessageCard has limited markdown support. Headings like '##' are not supported
|
||||
// and single newlines can collapse. Convert common patterns to a structured card.
|
||||
const lines: Array<string> = markdown
|
||||
.split("\n")
|
||||
.map((l: string) => {
|
||||
return l.trim();
|
||||
})
|
||||
).id;
|
||||
|
||||
return this.inviteUserToChannelByChannelId({
|
||||
authToken: data.authToken,
|
||||
channelId: channelId,
|
||||
workspaceUserId: data.workspaceUserId,
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async createChannelsIfDoesNotExist(data: {
|
||||
authToken: string;
|
||||
channelNames: Array<string>;
|
||||
}): Promise<Array<WorkspaceChannel>> {
|
||||
logger.debug("Creating channels if they do not exist with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const workspaceChannels: Array<WorkspaceChannel> = [];
|
||||
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
.filter((l: string) => {
|
||||
return l.length > 0;
|
||||
});
|
||||
|
||||
logger.debug("Existing workspace channels:");
|
||||
logger.debug(existingWorkspaceChannels);
|
||||
let title: string = "";
|
||||
const facts: Array<JSONObject> = [];
|
||||
const actions: Array<JSONObject> = [];
|
||||
const bodyTextParts: Array<string> = [];
|
||||
|
||||
for (let channelName of data.channelNames) {
|
||||
// if channel name starts with #, remove it
|
||||
if (channelName && channelName.startsWith("#")) {
|
||||
channelName = channelName.substring(1);
|
||||
}
|
||||
|
||||
// convert channel name to lowercase
|
||||
channelName = channelName.toLowerCase();
|
||||
|
||||
// replace spaces with hyphens
|
||||
channelName = channelName.replace(/\s+/g, "-");
|
||||
|
||||
if (existingWorkspaceChannels[channelName]) {
|
||||
logger.debug(`Channel ${channelName} already exists.`);
|
||||
workspaceChannels.push(existingWorkspaceChannels[channelName]!);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(`Channel ${channelName} does not exist. Creating channel.`);
|
||||
const channel: WorkspaceChannel = await this.createChannel({
|
||||
authToken: data.authToken,
|
||||
channelName: channelName,
|
||||
});
|
||||
|
||||
if (channel) {
|
||||
logger.debug(`Channel ${channelName} created successfully.`);
|
||||
workspaceChannels.push(channel);
|
||||
}
|
||||
// Extract title from the first non-empty line and strip markdown heading markers
|
||||
if (lines.length > 0) {
|
||||
const firstLine: string = lines[0] ?? "";
|
||||
title = firstLine
|
||||
.replace(/^#+\s*/, "") // remove leading markdown headers like ##
|
||||
.replace(/^\*\*|\*\*$/g, "") // remove stray bold markers if any
|
||||
.trim();
|
||||
lines.shift();
|
||||
}
|
||||
|
||||
logger.debug("Channels created or found:");
|
||||
logger.debug(workspaceChannels);
|
||||
return workspaceChannels;
|
||||
}
|
||||
const linkRegex: RegExp = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](url)
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async getWorkspaceChannelFromChannelName(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
logger.debug("Getting workspace channel ID from channel name with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const channels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
|
||||
logger.debug("All workspace channels:");
|
||||
logger.debug(channels);
|
||||
|
||||
if (!channels[data.channelName]) {
|
||||
logger.error("Channel not found.");
|
||||
throw new BadDataException("Channel not found.");
|
||||
}
|
||||
|
||||
logger.debug("Workspace channel ID obtained:");
|
||||
logger.debug(channels[data.channelName]!.id);
|
||||
|
||||
return channels[data.channelName]!;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async getWorkspaceChannelFromChannelId(data: {
|
||||
authToken: string;
|
||||
channelId: string;
|
||||
}): Promise<WorkspaceChannel> {
|
||||
logger.debug("Getting workspace channel from channel ID with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.get<JSONObject>(
|
||||
URL.fromString(
|
||||
`https://graph.microsoft.com/v1.0/teams/${data.channelId}`,
|
||||
),
|
||||
{
|
||||
Authorization: `Bearer ${data.authToken}`,
|
||||
},
|
||||
);
|
||||
|
||||
logger.debug("Response from Microsoft Graph API for getting channel info:");
|
||||
logger.debug(response);
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
logger.error("Error response from Microsoft Graph API:");
|
||||
logger.error(response);
|
||||
throw response;
|
||||
}
|
||||
|
||||
if (!(response.jsonData as JSONObject)?.["displayName"]) {
|
||||
logger.error("Invalid response from Microsoft Graph API:");
|
||||
logger.error(response.jsonData);
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
|
||||
const channel: WorkspaceChannel = {
|
||||
name: (response.jsonData as JSONObject)["displayName"] as string,
|
||||
id: data.channelId,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
};
|
||||
|
||||
logger.debug("Workspace channel obtained:");
|
||||
logger.debug(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async doesChannelExist(data: {
|
||||
authToken: string;
|
||||
channelName: string;
|
||||
}): Promise<boolean> {
|
||||
// if channel name starts with #, remove it
|
||||
if (data.channelName && data.channelName.startsWith("#")) {
|
||||
data.channelName = data.channelName.substring(1);
|
||||
}
|
||||
|
||||
// convert channel name to lowercase
|
||||
data.channelName = data.channelName.toLowerCase();
|
||||
|
||||
// get channel id from channel name
|
||||
const channels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
|
||||
// if this channel exists
|
||||
if (channels[data.channelName]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async sendMessage(data: {
|
||||
workspaceMessagePayload: WorkspaceMessagePayload;
|
||||
authToken: string; // which auth token should we use to send.
|
||||
userId: string;
|
||||
}): Promise<WorkspaceSendMessageResponse> {
|
||||
logger.debug("Sending message to Microsoft Teams with data:");
|
||||
logger.debug(data);
|
||||
|
||||
const blocks: Array<JSONObject> = this.getBlocksFromWorkspaceMessagePayload(
|
||||
data.workspaceMessagePayload,
|
||||
);
|
||||
|
||||
logger.debug("Blocks generated from workspace message payload:");
|
||||
logger.debug(blocks);
|
||||
|
||||
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
|
||||
await this.getAllWorkspaceChannels({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
|
||||
logger.debug("Existing workspace channels:");
|
||||
logger.debug(existingWorkspaceChannels);
|
||||
|
||||
const workspaceChannelsToPostTo: Array<WorkspaceChannel> = [];
|
||||
|
||||
for (let channelName of data.workspaceMessagePayload.channelNames) {
|
||||
if (channelName && channelName.startsWith("#")) {
|
||||
// trim # from channel name
|
||||
channelName = channelName.substring(1);
|
||||
}
|
||||
|
||||
let channel: WorkspaceChannel | null = null;
|
||||
|
||||
if (existingWorkspaceChannels[channelName]) {
|
||||
channel = existingWorkspaceChannels[channelName]!;
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
workspaceChannelsToPostTo.push(channel);
|
||||
} else {
|
||||
logger.debug(`Channel ${channelName} does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
// add channel ids.
|
||||
for (const channelId of data.workspaceMessagePayload.channelIds) {
|
||||
try {
|
||||
// Get the channel info including name from channel ID
|
||||
const channel: WorkspaceChannel =
|
||||
await this.getWorkspaceChannelFromChannelId({
|
||||
authToken: data.authToken,
|
||||
channelId: channelId,
|
||||
});
|
||||
|
||||
workspaceChannelsToPostTo.push(channel);
|
||||
} catch (err) {
|
||||
logger.error(`Error getting channel info for channel ID ${channelId}:`);
|
||||
logger.error(err);
|
||||
|
||||
// Fallback: create channel object with empty name if API call fails
|
||||
const channel: WorkspaceChannel = {
|
||||
id: channelId,
|
||||
name: channelId,
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
};
|
||||
|
||||
workspaceChannelsToPostTo.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Channel IDs to post to:");
|
||||
logger.debug(workspaceChannelsToPostTo);
|
||||
|
||||
const workspaceMessageResponse: WorkspaceSendMessageResponse = {
|
||||
threads: [],
|
||||
workspaceType: WorkspaceType.MicrosoftTeams,
|
||||
};
|
||||
|
||||
for (const channel of workspaceChannelsToPostTo) {
|
||||
try {
|
||||
// check if the user is in the channel.
|
||||
const isUserInChannel: boolean = await this.isUserInChannel({
|
||||
authToken: data.authToken,
|
||||
channelId: channel.id,
|
||||
userId: data.userId,
|
||||
for (const line of lines) {
|
||||
// Extract links to actions and strip them from text
|
||||
let lineWithoutLinks: string = line;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while ((match = linkRegex.exec(line))) {
|
||||
const name: string = match[1] ?? "";
|
||||
const url: string = match[2] ?? "";
|
||||
actions.push({
|
||||
["@type"]: "OpenUri",
|
||||
name: name,
|
||||
targets: [
|
||||
{
|
||||
os: "default",
|
||||
uri: url,
|
||||
},
|
||||
],
|
||||
});
|
||||
lineWithoutLinks = lineWithoutLinks.replace(match[0], "").trim();
|
||||
}
|
||||
|
||||
if (!isUserInChannel) {
|
||||
// add user to the channel
|
||||
await this.joinChannel({
|
||||
authToken: data.authToken,
|
||||
channelId: channel.id,
|
||||
});
|
||||
// Parse facts of the form **Label:** value
|
||||
const factMatch: RegExpExecArray | null = new RegExp(
|
||||
"\\*\\*(.*?):\\*\\*\\s*(.*)",
|
||||
).exec(lineWithoutLinks);
|
||||
|
||||
if (factMatch) {
|
||||
const name: string = (factMatch[1] ?? "").trim();
|
||||
const value: string = (factMatch[2] ?? "").trim();
|
||||
if (
|
||||
name.toLowerCase() === "description" ||
|
||||
name.toLowerCase() === "note"
|
||||
) {
|
||||
bodyTextParts.push(`**${name}:** ${value}`);
|
||||
} else {
|
||||
facts.push({ name: name, value: value });
|
||||
}
|
||||
|
||||
const thread: WorkspaceThread = await this.sendPayloadBlocksToChannel({
|
||||
authToken: data.authToken,
|
||||
workspaceChannel: channel,
|
||||
blocks: blocks,
|
||||
});
|
||||
|
||||
workspaceMessageResponse.threads.push(thread);
|
||||
|
||||
logger.debug(`Message sent to channel ID ${channel.id} successfully.`);
|
||||
} catch (e) {
|
||||
logger.error(`Error sending message to channel ID ${channel.id}:`);
|
||||
logger.error(e);
|
||||
} else if (lineWithoutLinks) {
|
||||
bodyTextParts.push(lineWithoutLinks);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Message sent successfully.");
|
||||
logger.debug(workspaceMessageResponse);
|
||||
const payload: JSONObject = {
|
||||
["@type"]: "MessageCard",
|
||||
["@context"]: "https://schema.org/extensions",
|
||||
title: title,
|
||||
summary: title,
|
||||
};
|
||||
|
||||
return workspaceMessageResponse;
|
||||
if (bodyTextParts.length > 0) {
|
||||
payload["text"] = bodyTextParts.join("\n\n");
|
||||
}
|
||||
|
||||
if (facts.length > 0) {
|
||||
payload["sections"] = [
|
||||
{
|
||||
facts: facts,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
payload["potentialAction"] = actions;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static override async sendMessageToChannelViaIncomingWebhook(data: {
|
||||
url: URL;
|
||||
text: string;
|
||||
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
|
||||
logger.debug("Sending message to Teams channel via incoming webhook:");
|
||||
logger.debug(data);
|
||||
|
||||
// Build a structured MessageCard from markdown for better rendering in Teams
|
||||
const payload: JSONObject = this.buildMessageCardFromMarkdown(data.text);
|
||||
|
||||
const apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null =
|
||||
await API.post(data.url, payload);
|
||||
|
||||
if (!apiResult) {
|
||||
logger.error(
|
||||
"Could not send message to Teams channel via incoming webhook.",
|
||||
);
|
||||
throw new Error(
|
||||
"Could not send message to Teams channel via incoming webhook.",
|
||||
);
|
||||
}
|
||||
|
||||
if (apiResult instanceof HTTPErrorResponse) {
|
||||
logger.error(
|
||||
"Error sending message to Teams channel via incoming webhook:",
|
||||
);
|
||||
logger.error(apiResult);
|
||||
throw apiResult;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"Message sent to Teams channel via incoming webhook successfully:",
|
||||
);
|
||||
logger.debug(apiResult);
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
public static isValidMicrosoftTeamsIncomingWebhookUrl(
|
||||
incomingWebhookUrl: URL,
|
||||
): boolean {
|
||||
// Check if the URL contains outlook.office.com or office.com webhook pattern
|
||||
const urlString: string = incomingWebhookUrl.toString();
|
||||
return (
|
||||
urlString.includes("outlook.office.com") ||
|
||||
urlString.includes("office.com")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
|
||||
<link rel="icon" href="/img/ou-wb.svg">
|
||||
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
|
||||
<link rel="canonical" href="/">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
|
||||
<meta property="og:url" content="https://oneuptime.com">
|
||||
|
||||
@@ -28,21 +28,10 @@ import EmptyResponseData from "../../../Types/API/EmptyResponse";
|
||||
|
||||
jest.setTimeout(60000); // Increase test timeout to 60 seconds becuase GitHub runners are slow
|
||||
|
||||
jest.mock("../../../Server/Services/ProjectUserService", () => {
|
||||
return {
|
||||
refreshProjectUsersByProject: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
// Get the mocked module to access the mock functions
|
||||
import ProjectUserService from "../../../Server/Services/ProjectUserService";
|
||||
const mockProjectUserService = ProjectUserService as jest.Mocked<typeof ProjectUserService>;
|
||||
|
||||
describe("TeamMemberService", () => {
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
// Re-setup the mock after resetAllMocks
|
||||
mockProjectUserService.refreshProjectUsersByProject.mockResolvedValue(undefined);
|
||||
await TestDatabaseMock.connectDbMock();
|
||||
});
|
||||
|
||||
|
||||
6
Common/Types/Exception/ExceptionMessages.ts
Normal file
6
Common/Types/Exception/ExceptionMessages.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
enum ExceptionMessages {
|
||||
MonitorNotFound = "Monitor not found.",
|
||||
MonitorDisabled = "Monitor is disabled. Please enable it to start monitoring again.",
|
||||
}
|
||||
|
||||
export default ExceptionMessages;
|
||||
@@ -420,9 +420,8 @@ const Detail: DetailFunction = <T extends GenericObject>(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(data === null || data === undefined) && field.placeholder && (
|
||||
<PlaceholderText text={field.placeholder} />
|
||||
)}
|
||||
{(data === null || data === undefined || data === "") &&
|
||||
field.placeholder && <PlaceholderText text={field.placeholder} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -102,10 +102,9 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
||||
code: (props: any) => {
|
||||
const { children, className, ...rest } = props;
|
||||
|
||||
// eslint-disable-next-line wrap-regex
|
||||
const match: RegExpExecArray | null = /language-(\w+)/.exec(
|
||||
className || "",
|
||||
);
|
||||
const match: RegExpExecArray | null = new RegExp(
|
||||
"language-(\\w+)",
|
||||
).exec(className || "");
|
||||
|
||||
const content: string = String(children as string).replace(
|
||||
/\n$/,
|
||||
|
||||
@@ -91,6 +91,10 @@ class BaseAPI extends API {
|
||||
return defaultHeaders;
|
||||
}
|
||||
|
||||
protected static logoutUser(): void {
|
||||
return User.logout();
|
||||
}
|
||||
|
||||
protected static override handleError(
|
||||
error: HTTPErrorResponse | APIException,
|
||||
): HTTPErrorResponse | APIException {
|
||||
@@ -103,7 +107,7 @@ class BaseAPI extends API {
|
||||
) {
|
||||
const loginRoute: Route = this.getLoginRoute();
|
||||
|
||||
User.logout();
|
||||
this.logoutUser();
|
||||
|
||||
if (Navigation.getQueryStringByName("token")) {
|
||||
Navigation.navigate(loginRoute.addRouteParam("sso", "true"), {
|
||||
|
||||
259
Common/UI/Utils/Countries.ts
Normal file
259
Common/UI/Utils/Countries.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
// ISO 3166-1 alpha-2 country codes and names.
|
||||
// Limited to widely recognized sovereign states and territories supported by Stripe.
|
||||
// If needed, expand or adjust for specific business logic.
|
||||
export interface CountryOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const Countries: Array<CountryOption> = [
|
||||
{ value: "AF", label: "Afghanistan" },
|
||||
{ value: "AL", label: "Albania" },
|
||||
{ value: "DZ", label: "Algeria" },
|
||||
{ value: "AS", label: "American Samoa" },
|
||||
{ value: "AD", label: "Andorra" },
|
||||
{ value: "AO", label: "Angola" },
|
||||
{ value: "AI", label: "Anguilla" },
|
||||
{ value: "AQ", label: "Antarctica" },
|
||||
{ value: "AG", label: "Antigua and Barbuda" },
|
||||
{ value: "AR", label: "Argentina" },
|
||||
{ value: "AM", label: "Armenia" },
|
||||
{ value: "AW", label: "Aruba" },
|
||||
{ value: "AU", label: "Australia" },
|
||||
{ value: "AT", label: "Austria" },
|
||||
{ value: "AZ", label: "Azerbaijan" },
|
||||
{ value: "BS", label: "Bahamas" },
|
||||
{ value: "BH", label: "Bahrain" },
|
||||
{ value: "BD", label: "Bangladesh" },
|
||||
{ value: "BB", label: "Barbados" },
|
||||
{ value: "BY", label: "Belarus" },
|
||||
{ value: "BE", label: "Belgium" },
|
||||
{ value: "BZ", label: "Belize" },
|
||||
{ value: "BJ", label: "Benin" },
|
||||
{ value: "BM", label: "Bermuda" },
|
||||
{ value: "BT", label: "Bhutan" },
|
||||
{ value: "BO", label: "Bolivia" },
|
||||
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba" },
|
||||
{ value: "BA", label: "Bosnia and Herzegovina" },
|
||||
{ value: "BW", label: "Botswana" },
|
||||
{ value: "BV", label: "Bouvet Island" },
|
||||
{ value: "BR", label: "Brazil" },
|
||||
{ value: "IO", label: "British Indian Ocean Territory" },
|
||||
{ value: "BN", label: "Brunei Darussalam" },
|
||||
{ value: "BG", label: "Bulgaria" },
|
||||
{ value: "BF", label: "Burkina Faso" },
|
||||
{ value: "BI", label: "Burundi" },
|
||||
{ value: "KH", label: "Cambodia" },
|
||||
{ value: "CM", label: "Cameroon" },
|
||||
{ value: "CA", label: "Canada" },
|
||||
{ value: "CV", label: "Cape Verde" },
|
||||
{ value: "KY", label: "Cayman Islands" },
|
||||
{ value: "CF", label: "Central African Republic" },
|
||||
{ value: "TD", label: "Chad" },
|
||||
{ value: "CL", label: "Chile" },
|
||||
{ value: "CN", label: "China" },
|
||||
{ value: "CX", label: "Christmas Island" },
|
||||
{ value: "CC", label: "Cocos (Keeling) Islands" },
|
||||
{ value: "CO", label: "Colombia" },
|
||||
{ value: "KM", label: "Comoros" },
|
||||
{ value: "CG", label: "Congo" },
|
||||
{ value: "CD", label: "Congo, the Democratic Republic of the" },
|
||||
{ value: "CK", label: "Cook Islands" },
|
||||
{ value: "CR", label: "Costa Rica" },
|
||||
{ value: "CI", label: "Côte d'Ivoire" },
|
||||
{ value: "HR", label: "Croatia" },
|
||||
{ value: "CU", label: "Cuba" },
|
||||
{ value: "CW", label: "Curaçao" },
|
||||
{ value: "CY", label: "Cyprus" },
|
||||
{ value: "CZ", label: "Czech Republic" },
|
||||
{ value: "DK", label: "Denmark" },
|
||||
{ value: "DJ", label: "Djibouti" },
|
||||
{ value: "DM", label: "Dominica" },
|
||||
{ value: "DO", label: "Dominican Republic" },
|
||||
{ value: "EC", label: "Ecuador" },
|
||||
{ value: "EG", label: "Egypt" },
|
||||
{ value: "SV", label: "El Salvador" },
|
||||
{ value: "GQ", label: "Equatorial Guinea" },
|
||||
{ value: "ER", label: "Eritrea" },
|
||||
{ value: "EE", label: "Estonia" },
|
||||
{ value: "ET", label: "Ethiopia" },
|
||||
{ value: "FK", label: "Falkland Islands (Malvinas)" },
|
||||
{ value: "FO", label: "Faroe Islands" },
|
||||
{ value: "FJ", label: "Fiji" },
|
||||
{ value: "FI", label: "Finland" },
|
||||
{ value: "FR", label: "France" },
|
||||
{ value: "GF", label: "French Guiana" },
|
||||
{ value: "PF", label: "French Polynesia" },
|
||||
{ value: "TF", label: "French Southern Territories" },
|
||||
{ value: "GA", label: "Gabon" },
|
||||
{ value: "GM", label: "Gambia" },
|
||||
{ value: "GE", label: "Georgia" },
|
||||
{ value: "DE", label: "Germany" },
|
||||
{ value: "GH", label: "Ghana" },
|
||||
{ value: "GI", label: "Gibraltar" },
|
||||
{ value: "GR", label: "Greece" },
|
||||
{ value: "GL", label: "Greenland" },
|
||||
{ value: "GD", label: "Grenada" },
|
||||
{ value: "GP", label: "Guadeloupe" },
|
||||
{ value: "GU", label: "Guam" },
|
||||
{ value: "GT", label: "Guatemala" },
|
||||
{ value: "GG", label: "Guernsey" },
|
||||
{ value: "GN", label: "Guinea" },
|
||||
{ value: "GW", label: "Guinea-Bissau" },
|
||||
{ value: "GY", label: "Guyana" },
|
||||
{ value: "HT", label: "Haiti" },
|
||||
{ value: "HM", label: "Heard Island and McDonald Islands" },
|
||||
{ value: "HN", label: "Honduras" },
|
||||
{ value: "HK", label: "Hong Kong" },
|
||||
{ value: "HU", label: "Hungary" },
|
||||
{ value: "IS", label: "Iceland" },
|
||||
{ value: "IN", label: "India" },
|
||||
{ value: "ID", label: "Indonesia" },
|
||||
{ value: "IR", label: "Iran, Islamic Republic of" },
|
||||
{ value: "IQ", label: "Iraq" },
|
||||
{ value: "IE", label: "Ireland" },
|
||||
{ value: "IM", label: "Isle of Man" },
|
||||
{ value: "IL", label: "Israel" },
|
||||
{ value: "IT", label: "Italy" },
|
||||
{ value: "JM", label: "Jamaica" },
|
||||
{ value: "JP", label: "Japan" },
|
||||
{ value: "JE", label: "Jersey" },
|
||||
{ value: "JO", label: "Jordan" },
|
||||
{ value: "KZ", label: "Kazakhstan" },
|
||||
{ value: "KE", label: "Kenya" },
|
||||
{ value: "KI", label: "Kiribati" },
|
||||
{ value: "KP", label: "Korea, Democratic People's Republic of" },
|
||||
{ value: "KR", label: "Korea, Republic of" },
|
||||
{ value: "KW", label: "Kuwait" },
|
||||
{ value: "KG", label: "Kyrgyzstan" },
|
||||
{ value: "LA", label: "Lao People's Democratic Republic" },
|
||||
{ value: "LV", label: "Latvia" },
|
||||
{ value: "LB", label: "Lebanon" },
|
||||
{ value: "LS", label: "Lesotho" },
|
||||
{ value: "LR", label: "Liberia" },
|
||||
{ value: "LY", label: "Libya" },
|
||||
{ value: "LI", label: "Liechtenstein" },
|
||||
{ value: "LT", label: "Lithuania" },
|
||||
{ value: "LU", label: "Luxembourg" },
|
||||
{ value: "MO", label: "Macao" },
|
||||
{ value: "MG", label: "Madagascar" },
|
||||
{ value: "MW", label: "Malawi" },
|
||||
{ value: "MY", label: "Malaysia" },
|
||||
{ value: "MV", label: "Maldives" },
|
||||
{ value: "ML", label: "Mali" },
|
||||
{ value: "MT", label: "Malta" },
|
||||
{ value: "MH", label: "Marshall Islands" },
|
||||
{ value: "MQ", label: "Martinique" },
|
||||
{ value: "MR", label: "Mauritania" },
|
||||
{ value: "MU", label: "Mauritius" },
|
||||
{ value: "YT", label: "Mayotte" },
|
||||
{ value: "MX", label: "Mexico" },
|
||||
{ value: "FM", label: "Micronesia, Federated States of" },
|
||||
{ value: "MD", label: "Moldova, Republic of" },
|
||||
{ value: "MC", label: "Monaco" },
|
||||
{ value: "MN", label: "Mongolia" },
|
||||
{ value: "ME", label: "Montenegro" },
|
||||
{ value: "MS", label: "Montserrat" },
|
||||
{ value: "MA", label: "Morocco" },
|
||||
{ value: "MZ", label: "Mozambique" },
|
||||
{ value: "MM", label: "Myanmar" },
|
||||
{ value: "NA", label: "Namibia" },
|
||||
{ value: "NR", label: "Nauru" },
|
||||
{ value: "NP", label: "Nepal" },
|
||||
{ value: "NL", label: "Netherlands" },
|
||||
{ value: "NC", label: "New Caledonia" },
|
||||
{ value: "NZ", label: "New Zealand" },
|
||||
{ value: "NI", label: "Nicaragua" },
|
||||
{ value: "NE", label: "Niger" },
|
||||
{ value: "NG", label: "Nigeria" },
|
||||
{ value: "NU", label: "Niue" },
|
||||
{ value: "NF", label: "Norfolk Island" },
|
||||
{ value: "MK", label: "North Macedonia" },
|
||||
{ value: "MP", label: "Northern Mariana Islands" },
|
||||
{ value: "NO", label: "Norway" },
|
||||
{ value: "OM", label: "Oman" },
|
||||
{ value: "PK", label: "Pakistan" },
|
||||
{ value: "PW", label: "Palau" },
|
||||
{ value: "PS", label: "Palestine, State of" },
|
||||
{ value: "PA", label: "Panama" },
|
||||
{ value: "PG", label: "Papua New Guinea" },
|
||||
{ value: "PY", label: "Paraguay" },
|
||||
{ value: "PE", label: "Peru" },
|
||||
{ value: "PH", label: "Philippines" },
|
||||
{ value: "PN", label: "Pitcairn" },
|
||||
{ value: "PL", label: "Poland" },
|
||||
{ value: "PT", label: "Portugal" },
|
||||
{ value: "PR", label: "Puerto Rico" },
|
||||
{ value: "QA", label: "Qatar" },
|
||||
{ value: "RE", label: "Réunion" },
|
||||
{ value: "RO", label: "Romania" },
|
||||
{ value: "RU", label: "Russian Federation" },
|
||||
{ value: "RW", label: "Rwanda" },
|
||||
{ value: "BL", label: "Saint Barthélemy" },
|
||||
{ value: "SH", label: "Saint Helena, Ascension and Tristan da Cunha" },
|
||||
{ value: "KN", label: "Saint Kitts and Nevis" },
|
||||
{ value: "LC", label: "Saint Lucia" },
|
||||
{ value: "MF", label: "Saint Martin (French part)" },
|
||||
{ value: "PM", label: "Saint Pierre and Miquelon" },
|
||||
{ value: "VC", label: "Saint Vincent and the Grenadines" },
|
||||
{ value: "WS", label: "Samoa" },
|
||||
{ value: "SM", label: "San Marino" },
|
||||
{ value: "ST", label: "Sao Tome and Principe" },
|
||||
{ value: "SA", label: "Saudi Arabia" },
|
||||
{ value: "SN", label: "Senegal" },
|
||||
{ value: "RS", label: "Serbia" },
|
||||
{ value: "SC", label: "Seychelles" },
|
||||
{ value: "SL", label: "Sierra Leone" },
|
||||
{ value: "SG", label: "Singapore" },
|
||||
{ value: "SX", label: "Sint Maarten (Dutch part)" },
|
||||
{ value: "SK", label: "Slovakia" },
|
||||
{ value: "SI", label: "Slovenia" },
|
||||
{ value: "SB", label: "Solomon Islands" },
|
||||
{ value: "SO", label: "Somalia" },
|
||||
{ value: "ZA", label: "South Africa" },
|
||||
{ value: "GS", label: "South Georgia and the South Sandwich Islands" },
|
||||
{ value: "SS", label: "South Sudan" },
|
||||
{ value: "ES", label: "Spain" },
|
||||
{ value: "LK", label: "Sri Lanka" },
|
||||
{ value: "SD", label: "Sudan" },
|
||||
{ value: "SR", label: "Suriname" },
|
||||
{ value: "SJ", label: "Svalbard and Jan Mayen" },
|
||||
{ value: "SZ", label: "Swaziland" },
|
||||
{ value: "SE", label: "Sweden" },
|
||||
{ value: "CH", label: "Switzerland" },
|
||||
{ value: "SY", label: "Syrian Arab Republic" },
|
||||
{ value: "TW", label: "Taiwan, Province of China" },
|
||||
{ value: "TJ", label: "Tajikistan" },
|
||||
{ value: "TZ", label: "Tanzania, United Republic of" },
|
||||
{ value: "TH", label: "Thailand" },
|
||||
{ value: "TL", label: "Timor-Leste" },
|
||||
{ value: "TG", label: "Togo" },
|
||||
{ value: "TK", label: "Tokelau" },
|
||||
{ value: "TO", label: "Tonga" },
|
||||
{ value: "TT", label: "Trinidad and Tobago" },
|
||||
{ value: "TN", label: "Tunisia" },
|
||||
{ value: "TR", label: "Turkey" },
|
||||
{ value: "TM", label: "Turkmenistan" },
|
||||
{ value: "TC", label: "Turks and Caicos Islands" },
|
||||
{ value: "TV", label: "Tuvalu" },
|
||||
{ value: "UG", label: "Uganda" },
|
||||
{ value: "UA", label: "Ukraine" },
|
||||
{ value: "AE", label: "United Arab Emirates" },
|
||||
{ value: "GB", label: "United Kingdom" },
|
||||
{ value: "US", label: "United States" },
|
||||
{ value: "UM", label: "United States Minor Outlying Islands" },
|
||||
{ value: "UY", label: "Uruguay" },
|
||||
{ value: "UZ", label: "Uzbekistan" },
|
||||
{ value: "VU", label: "Vanuatu" },
|
||||
{ value: "VE", label: "Venezuela, Bolivarian Republic of" },
|
||||
{ value: "VN", label: "Viet Nam" },
|
||||
{ value: "VG", label: "Virgin Islands, British" },
|
||||
{ value: "VI", label: "Virgin Islands, U.S." },
|
||||
{ value: "WF", label: "Wallis and Futuna" },
|
||||
{ value: "EH", label: "Western Sahara" },
|
||||
{ value: "YE", label: "Yemen" },
|
||||
{ value: "ZM", label: "Zambia" },
|
||||
{ value: "ZW", label: "Zimbabwe" },
|
||||
];
|
||||
|
||||
export default Countries;
|
||||
@@ -4,5 +4,7 @@
|
||||
"../Common"
|
||||
],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import React, {
|
||||
} from "react";
|
||||
import MonitorStatusElement from "./MonitorStatusElement";
|
||||
import Loader, { LoaderType } from "Common/UI/Components/Loader/Loader";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorId: ObjectID;
|
||||
@@ -45,7 +46,7 @@ const GetMonitorStatusElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
if (!monitor) {
|
||||
setIsLoading(false);
|
||||
setError("Monitor not found");
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,33 @@ const CustomProbeDocumentation: FunctionComponent<ComponentProps> = (
|
||||
docker run --name oneuptime-probe --network host -e PROBE_KEY=${props.probeKey.toString()} -e PROBE_ID=${props.probeId.toString()} -e ONEUPTIME_URL=${host.toString()} -d oneuptime/probe:release
|
||||
`}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
||||
With Proxy Configuration (Optional)
|
||||
</h4>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
code={`
|
||||
# With HTTP/HTTPS proxy
|
||||
docker run --name oneuptime-probe --network host \\
|
||||
-e PROBE_KEY=${props.probeKey.toString()} \\
|
||||
-e PROBE_ID=${props.probeId.toString()} \\
|
||||
-e ONEUPTIME_URL=${host.toString()} \\
|
||||
-e HTTP_PROXY_URL=http://proxy.example.com:8080 \\
|
||||
-e HTTPS_PROXY_URL=http://proxy.example.com:8080 \\
|
||||
-d oneuptime/probe:release
|
||||
|
||||
# With proxy authentication
|
||||
docker run --name oneuptime-probe --network host \\
|
||||
-e PROBE_KEY=${props.probeKey.toString()} \\
|
||||
-e PROBE_ID=${props.probeId.toString()} \\
|
||||
-e ONEUPTIME_URL=${host.toString()} \\
|
||||
-e HTTP_PROXY_URL=http://username:password@proxy.example.com:8080 \\
|
||||
-e HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080 \\
|
||||
-d oneuptime/probe:release
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import { useAsyncEffect } from "use-async-effect";
|
||||
import MonitorTestForm from "../../../Components/Form/Monitor/MonitorTest";
|
||||
import Probe from "Common/Models/DatabaseModels/Probe";
|
||||
@@ -61,7 +62,7 @@ const MonitorCriteria: FunctionComponent<
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
|
||||
const MonitorDocumentation: FunctionComponent<
|
||||
@@ -55,7 +56,7 @@ const MonitorDocumentation: FunctionComponent<
|
||||
setMonitor(item);
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
@@ -215,7 +216,7 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
|
||||
const MonitorCriteria: FunctionComponent<
|
||||
@@ -48,7 +49,7 @@ const MonitorCriteria: FunctionComponent<
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Monitor from "Common/Models/DatabaseModels/Monitor";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
|
||||
@@ -43,7 +44,7 @@ const MonitorViewLayout: FunctionComponent = (): ReactElement => {
|
||||
setMonitor(item);
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import AnalyticsModelTable from "Common/UI/Components/ModelTable/AnalyticsModelTable";
|
||||
import SummaryInfo from "../../../Components/Monitor/SummaryView/SummaryInfo";
|
||||
@@ -55,7 +56,7 @@ const MonitorLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import React, {
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import SummaryInfo from "../../../Components/Monitor/SummaryView/SummaryInfo";
|
||||
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
|
||||
const MonitorProbes: FunctionComponent<
|
||||
PageComponentProps
|
||||
@@ -61,7 +62,7 @@ const MonitorProbes: FunctionComponent<
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
|
||||
@@ -61,7 +62,7 @@ const MonitorCriteria: FunctionComponent<
|
||||
});
|
||||
|
||||
if (!monitor) {
|
||||
setError(`Monitor not found`);
|
||||
setError(ExceptionMessages.MonitorNotFound);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
import Countries from "Common/UI/Utils/Countries";
|
||||
|
||||
export type ComponentProps = PageComponentProps;
|
||||
|
||||
@@ -523,6 +524,89 @@ const Settings: FunctionComponent<ComponentProps> = (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<CardModelDetail<Project>
|
||||
name="Business Details"
|
||||
cardProps={{
|
||||
title: "Business Details / Billing Address",
|
||||
description:
|
||||
"Enter your business legal name, address and optional tax info. This will appear on your invoices.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText={"Update"}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
businessDetails: true,
|
||||
},
|
||||
title: "Business Details / Billing Address",
|
||||
description:
|
||||
"This information will appear on invoices. Include company legal name, address, and tax / VAT ID if applicable.",
|
||||
required: false,
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
validation: {
|
||||
maxLength: 10000,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
businessDetailsCountry: true,
|
||||
},
|
||||
title: "Country",
|
||||
description: "Required by Stripe. Select your billing country.",
|
||||
required: false,
|
||||
placeholder: "Select Country",
|
||||
fieldType: FormFieldSchemaType.Dropdown,
|
||||
dropdownOptions: Countries,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
financeAccountingEmail: true,
|
||||
},
|
||||
title: "Finance / Accounting Email",
|
||||
description:
|
||||
"Invoices, receipts and billing notifications will be sent here (optional).",
|
||||
required: false,
|
||||
placeholder: "finance@yourcompany.com",
|
||||
fieldType: FormFieldSchemaType.Email,
|
||||
validation: {
|
||||
minLength: 3,
|
||||
maxLength: 200,
|
||||
},
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: Project,
|
||||
id: "model-detail-project-business-details",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
businessDetails: true,
|
||||
},
|
||||
title: "Business Details / Billing Address",
|
||||
placeholder: "No business details added yet.",
|
||||
fieldType: FieldType.LongText,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
businessDetailsCountry: true,
|
||||
},
|
||||
title: "Country",
|
||||
placeholder: "No country details added yet.",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
financeAccountingEmail: true,
|
||||
},
|
||||
title: "Finance / Accounting Email",
|
||||
placeholder: "No finance / accounting email added yet.",
|
||||
fieldType: FieldType.Email,
|
||||
},
|
||||
],
|
||||
modelId: ProjectUtil.getCurrentProjectId()!,
|
||||
}}
|
||||
/>
|
||||
|
||||
{!reseller && (
|
||||
<Card
|
||||
title={`Cancel Plan`}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import UserElement from "../../Components/User/User";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import Team from "Common/Models/DatabaseModels/Team";
|
||||
import TeamsElement from "../../Components/Team/TeamsElement";
|
||||
import TeamElement from "../../Components/Team/Team";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import { RouteUtil } from "../../Utils/RouteMap";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
@@ -16,6 +15,8 @@ import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import { FormType } from "Common/UI/Components/Forms/ModelForm";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import { Green, Yellow } from "Common/Types/BrandColors";
|
||||
|
||||
const Teams: FunctionComponent<PageComponentProps> = (
|
||||
props: PageComponentProps,
|
||||
@@ -26,21 +27,22 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelTable<ProjectUser>
|
||||
modelType={ProjectUser}
|
||||
<ModelTable<TeamMember>
|
||||
modelType={TeamMember}
|
||||
id="teams-table"
|
||||
name="Settings > Users"
|
||||
userPreferencesKey="users-table"
|
||||
isDeleteable={false}
|
||||
isDeleteable={true}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
isCreateable={false}
|
||||
onFilterApplied={(isApplied: boolean) => {
|
||||
setIsFilterApplied(isApplied);
|
||||
}}
|
||||
isViewable={true}
|
||||
cardProps={{
|
||||
title: "Users",
|
||||
description: "Here is a list of all the users in this project.",
|
||||
description:
|
||||
"Here is a list of all the team members in this project.",
|
||||
buttons: [
|
||||
{
|
||||
title: "Invite User",
|
||||
@@ -61,7 +63,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
}}
|
||||
showRefreshButton={true}
|
||||
onViewPage={(item: ProjectUser) => {
|
||||
onViewPage={(item: TeamMember) => {
|
||||
const viewPageRoute: string =
|
||||
RouteUtil.populateRouteParams(props.pageRoute).toString() +
|
||||
"/" +
|
||||
@@ -72,12 +74,12 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
acceptedTeams: {
|
||||
team: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Teams member of",
|
||||
type: FieldType.EntityArray,
|
||||
title: "Team",
|
||||
type: FieldType.Entity,
|
||||
filterEntityType: Team,
|
||||
filterQuery: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
@@ -89,20 +91,10 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
field: {
|
||||
invitedTeams: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
title: "Teams invited to",
|
||||
type: FieldType.EntityArray,
|
||||
filterEntityType: Team,
|
||||
filterQuery: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
},
|
||||
filterDropdownField: {
|
||||
label: "name",
|
||||
value: "_id",
|
||||
hasAcceptedInvitation: true,
|
||||
},
|
||||
title: "Status",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
@@ -116,7 +108,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
title: "User",
|
||||
type: FieldType.Element,
|
||||
getElement: (item: ProjectUser) => {
|
||||
getElement: (item: TeamMember) => {
|
||||
if (!item.user) {
|
||||
return <p>User not found</p>;
|
||||
}
|
||||
@@ -125,26 +117,31 @@ const Teams: FunctionComponent<PageComponentProps> = (
|
||||
},
|
||||
{
|
||||
field: {
|
||||
acceptedTeams: {
|
||||
team: {
|
||||
name: true,
|
||||
_id: true,
|
||||
},
|
||||
},
|
||||
title: "Teams member of",
|
||||
title: "Team",
|
||||
type: FieldType.Element,
|
||||
getElement: (item: ProjectUser) => {
|
||||
return <TeamsElement teams={item.acceptedTeams || []} />;
|
||||
getElement: (item: TeamMember) => {
|
||||
if (!item.team) {
|
||||
return <p>No team assigned</p>;
|
||||
}
|
||||
return <TeamElement team={item.team!} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
invitedTeams: {
|
||||
name: true,
|
||||
},
|
||||
hasAcceptedInvitation: true,
|
||||
},
|
||||
title: "Teams invited to",
|
||||
title: "Status",
|
||||
type: FieldType.Element,
|
||||
getElement: (item: ProjectUser) => {
|
||||
return <TeamsElement teams={item.invitedTeams || []} />;
|
||||
getElement: (item: TeamMember) => {
|
||||
if (item.hasAcceptedInvitation) {
|
||||
return <Pill text="Member" color={Green} />;
|
||||
}
|
||||
return <Pill text="Invitation Sent" color={Yellow} />;
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import NotNull from "Common/Types/BaseDatabase/NotNull";
|
||||
import { Green, Red } from "Common/Types/BrandColors";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
|
||||
import { CategoryCheckboxOptionsAndCategories } from "Common/UI/Components/CategoryCheckbox/Index";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { ModelField } from "Common/UI/Components/Forms/ModelForm";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import SubscriberUtil from "Common/UI/Utils/StatusPage";
|
||||
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
|
||||
import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
|
||||
const StatusPageMicrosoftTeamsSubscribers: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (props: PageComponentProps): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
const [
|
||||
allowSubscribersToChooseResources,
|
||||
setAllowSubscribersToChooseResources,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const [
|
||||
allowSubscribersToChooseEventTypes,
|
||||
setAllowSubscribersToChooseEventTypes,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const [
|
||||
isMicrosoftTeamsSubscribersEnabled,
|
||||
setIsMicrosoftTeamsSubscribersEnabled,
|
||||
] = React.useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string>("");
|
||||
const [
|
||||
categoryCheckboxOptionsAndCategories,
|
||||
setCategoryCheckboxOptionsAndCategories,
|
||||
] = useState<CategoryCheckboxOptionsAndCategories>({
|
||||
categories: [],
|
||||
options: [],
|
||||
});
|
||||
|
||||
const fetchCheckboxOptionsAndCategories: PromiseVoidFunction =
|
||||
async (): Promise<void> => {
|
||||
const result: CategoryCheckboxOptionsAndCategories =
|
||||
await SubscriberUtil.getCategoryCheckboxPropsBasedOnResources(modelId);
|
||||
|
||||
setCategoryCheckboxOptionsAndCategories(result);
|
||||
};
|
||||
|
||||
const fetchStatusPage: PromiseVoidFunction = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const statusPage: StatusPage | null = await ModelAPI.getItem({
|
||||
modelType: StatusPage,
|
||||
id: modelId,
|
||||
select: {
|
||||
allowSubscribersToChooseResources: true,
|
||||
allowSubscribersToChooseEventTypes: true,
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (statusPage && statusPage.allowSubscribersToChooseResources) {
|
||||
setAllowSubscribersToChooseResources(
|
||||
statusPage.allowSubscribersToChooseResources,
|
||||
);
|
||||
await fetchCheckboxOptionsAndCategories();
|
||||
}
|
||||
|
||||
if (statusPage && statusPage.allowSubscribersToChooseEventTypes) {
|
||||
setAllowSubscribersToChooseEventTypes(
|
||||
statusPage.allowSubscribersToChooseEventTypes,
|
||||
);
|
||||
}
|
||||
|
||||
if (statusPage && statusPage.enableMicrosoftTeamsSubscribers) {
|
||||
setIsMicrosoftTeamsSubscribersEnabled(
|
||||
statusPage.enableMicrosoftTeamsSubscribers,
|
||||
);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatusPage().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [formFields, setFormFields] = React.useState<
|
||||
Array<ModelField<StatusPageSubscriber>>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
return; // don't do anything if loading
|
||||
}
|
||||
|
||||
const formFields: Array<ModelField<StatusPageSubscriber>> = [
|
||||
{
|
||||
field: {
|
||||
microsoftTeamsWorkspaceName: true,
|
||||
},
|
||||
stepId: "subscriber-info",
|
||||
title: "Microsoft Teams Workspace Name",
|
||||
description:
|
||||
"Name of the Microsoft Teams workspace for identification.",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "my-company-workspace",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
microsoftTeamsIncomingWebhookUrl: true,
|
||||
},
|
||||
stepId: "subscriber-info",
|
||||
title: "Microsoft Teams Incoming Webhook URL",
|
||||
description: "Status page updates will be sent to this Teams channel.",
|
||||
fieldType: FormFieldSchemaType.URL,
|
||||
required: true,
|
||||
placeholder: "https://xxxxx.office.com/webhook/...",
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
sendYouHaveSubscribedMessage: true,
|
||||
},
|
||||
title: "Send Subscription Notification",
|
||||
stepId: "subscriber-info",
|
||||
description:
|
||||
"Send a notification to the Teams channel confirming the subscription.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
doNotShowWhenEditing: true,
|
||||
},
|
||||
|
||||
{
|
||||
field: {
|
||||
isUnsubscribed: true,
|
||||
},
|
||||
title: "Unsubscribe",
|
||||
stepId: "subscriber-info",
|
||||
description: "Unsubscribe this Teams channel from the status page.",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
doNotShowWhenCreating: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (allowSubscribersToChooseResources) {
|
||||
formFields.push({
|
||||
field: {
|
||||
isSubscribedToAllResources: true,
|
||||
},
|
||||
title: "Subscribe to All Resources",
|
||||
stepId: "subscriber-info",
|
||||
description: "Send notifications for all resources.",
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
formFields.push({
|
||||
field: {
|
||||
statusPageResources: true,
|
||||
},
|
||||
title: "Select Resources to Subscribe",
|
||||
description: "Please select the resources you want to subscribe to.",
|
||||
stepId: "subscriber-info",
|
||||
fieldType: FormFieldSchemaType.CategoryCheckbox,
|
||||
required: false,
|
||||
categoryCheckboxProps: categoryCheckboxOptionsAndCategories,
|
||||
showIf: (model: FormValues<StatusPageSubscriber>) => {
|
||||
return !model || !model.isSubscribedToAllResources;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (allowSubscribersToChooseEventTypes) {
|
||||
formFields.push({
|
||||
field: {
|
||||
isSubscribedToAllEventTypes: true,
|
||||
},
|
||||
title: "Subscribe to All Event Types",
|
||||
stepId: "subscriber-info",
|
||||
description:
|
||||
"Select this option if you want to subscribe to all event types.",
|
||||
fieldType: FormFieldSchemaType.Checkbox,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
});
|
||||
|
||||
formFields.push({
|
||||
field: {
|
||||
statusPageEventTypes: true,
|
||||
},
|
||||
title: "Select Event Types to Subscribe",
|
||||
stepId: "subscriber-info",
|
||||
description: "Please select the event types you want to subscribe to.",
|
||||
fieldType: FormFieldSchemaType.MultiSelectDropdown,
|
||||
required: false,
|
||||
dropdownOptions: SubscriberUtil.getDropdownPropsBasedOnEventTypes(),
|
||||
showIf: (model: FormValues<StatusPageSubscriber>) => {
|
||||
return !model || !model.isSubscribedToAllEventTypes;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// add internal note field
|
||||
formFields.push({
|
||||
field: {
|
||||
internalNote: true,
|
||||
},
|
||||
title: "Internal Note",
|
||||
stepId: "internal-info",
|
||||
description:
|
||||
"Internal note for the subscriber. This is for internal use only and is visible only to the team members.",
|
||||
fieldType: FormFieldSchemaType.Markdown,
|
||||
required: false,
|
||||
});
|
||||
|
||||
setFormFields(formFields);
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isLoading ? <PageLoader isVisible={true} /> : <></>}
|
||||
|
||||
{error ? <ErrorMessage message={error} /> : <></>}
|
||||
|
||||
{!error && !isLoading ? (
|
||||
<>
|
||||
{!isMicrosoftTeamsSubscribersEnabled && (
|
||||
<Alert
|
||||
type={AlertType.DANGER}
|
||||
title="Microsoft Teams subscribers are not enabled for this status page. Please enable it in Subscriber Settings"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ModelTable<StatusPageSubscriber>
|
||||
modelType={StatusPageSubscriber}
|
||||
id="table-microsoft-teams-subscriber"
|
||||
name="Status Page > Microsoft Teams Subscribers"
|
||||
userPreferencesKey="status-page-microsoft-teams-subscribers-table"
|
||||
isDeleteable={true}
|
||||
showViewIdButton={true}
|
||||
isCreateable={true}
|
||||
isEditable={true}
|
||||
isViewable={false}
|
||||
selectMoreFields={{
|
||||
isSubscriptionConfirmed: true,
|
||||
}}
|
||||
query={{
|
||||
statusPageId: modelId,
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
microsoftTeamsWorkspaceName: new NotNull(),
|
||||
}}
|
||||
onBeforeCreate={(
|
||||
item: StatusPageSubscriber,
|
||||
): Promise<StatusPageSubscriber> => {
|
||||
if (!props.currentProject || !props.currentProject._id) {
|
||||
throw new BadDataException("Project ID cannot be null");
|
||||
}
|
||||
|
||||
item.statusPageId = modelId;
|
||||
item.projectId = new ObjectID(props.currentProject._id);
|
||||
return Promise.resolve(item);
|
||||
}}
|
||||
cardProps={{
|
||||
title: "Microsoft Teams Subscribers",
|
||||
description:
|
||||
"Here are the list of Microsoft Teams channels that have subscribed to the status page.",
|
||||
}}
|
||||
noItemsMessage={"No Microsoft Teams subscribers found."}
|
||||
formSteps={[
|
||||
{
|
||||
title: "Subscriber Info",
|
||||
id: "subscriber-info",
|
||||
},
|
||||
{
|
||||
title: "Internal Info",
|
||||
id: "internal-info",
|
||||
},
|
||||
]}
|
||||
formFields={formFields}
|
||||
showRefreshButton={true}
|
||||
viewPageRoute={Navigation.getCurrentRoute()}
|
||||
filters={[
|
||||
{
|
||||
field: {
|
||||
microsoftTeamsWorkspaceName: true,
|
||||
},
|
||||
title: "Teams Workspace Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
isUnsubscribed: true,
|
||||
},
|
||||
title: "Is Unsubscribed",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
isSubscriptionConfirmed: true,
|
||||
},
|
||||
title: "Subscription Confirmed?",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: "Subscribed At",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
microsoftTeamsWorkspaceName: true,
|
||||
},
|
||||
title: "Workspace Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
isUnsubscribed: true,
|
||||
},
|
||||
title: "Status",
|
||||
type: FieldType.Text,
|
||||
getElement: (item: StatusPageSubscriber): ReactElement => {
|
||||
if (item["isUnsubscribed"]) {
|
||||
return <Pill color={Red} text={"Unsubscribed"} />;
|
||||
}
|
||||
|
||||
if (!item["isSubscriptionConfirmed"]) {
|
||||
return (
|
||||
<Pill
|
||||
color={Red}
|
||||
text={"Awaiting Confirmation"}
|
||||
tooltip="Subscription not yet confirmed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Pill color={Green} text={"Subscribed"} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: {
|
||||
createdAt: true,
|
||||
},
|
||||
title: "Subscribed At",
|
||||
type: FieldType.DateTime,
|
||||
hideOnMobile: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPageMicrosoftTeamsSubscribers;
|
||||
@@ -111,6 +111,18 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Slack}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "MS Teams Subscribers",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[
|
||||
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS
|
||||
] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.MicrosoftTeams}
|
||||
/>
|
||||
|
||||
{/* <SideMenuItem
|
||||
link={{
|
||||
|
||||
@@ -127,6 +127,43 @@ const StatusPageDelete: FunctionComponent<
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail<StatusPage>
|
||||
name="Status Page > Branding > Subscriber > Microsoft Teams"
|
||||
cardProps={{
|
||||
title: "Microsoft Teams Subscribers",
|
||||
description:
|
||||
"Microsoft Teams subscriber settings for this status page.",
|
||||
}}
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
},
|
||||
title: "Enable Microsoft Teams Subscribers",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
placeholder:
|
||||
"Can Microsoft Teams subscribers subscribe to this status page?",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 1,
|
||||
modelType: StatusPage,
|
||||
id: "model-detail-microsoft-teams-subscribers",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
enableMicrosoftTeamsSubscribers: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Enable Microsoft Teams Subscribers",
|
||||
},
|
||||
],
|
||||
modelId: modelId,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail<StatusPage>
|
||||
name="Status Page > Branding > Subscriber > Advanced"
|
||||
cardProps={{
|
||||
|
||||
@@ -49,6 +49,11 @@ const StatusPagesViewSlackSubscribers: LazyExoticComponent<
|
||||
> = lazy(() => {
|
||||
return import("../Pages/StatusPages/View/SlackSubscribers");
|
||||
});
|
||||
const StatusPagesViewMicrosoftTeamsSubscribers: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/StatusPages/View/MicrosoftTeamsSubscribers");
|
||||
});
|
||||
const StatusPagesViewWebhookSubscribers: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -640,6 +645,24 @@ const StatusPagesRoutes: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS,
|
||||
)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<StatusPagesViewMicrosoftTeamsSubscribers
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[
|
||||
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS
|
||||
] as Route
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.STATUS_PAGE_VIEW_EMBEDDED)}
|
||||
element={
|
||||
|
||||
@@ -209,6 +209,7 @@ enum PageMap {
|
||||
STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS = "STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS",
|
||||
STATUS_PAGE_VIEW_SMS_SUBSCRIBERS = "STATUS_PAGE_VIEW_SMS_SUBSCRIBERS",
|
||||
STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS = "STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS",
|
||||
STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS = "STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS",
|
||||
STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS = "STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS",
|
||||
STATUS_PAGE_VIEW_RESOURCES = "STATUS_PAGE_VIEW_RESOURCES",
|
||||
STATUS_PAGE_VIEW_ADVANCED_OPTIONS = "STATUS_PAGE_VIEW_ADVANCED_OPTIONS",
|
||||
|
||||
@@ -128,6 +128,7 @@ export const StatusPagesRoutePath: Dictionary<string> = {
|
||||
[PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS]: `${RouteParams.ModelID}/email-subscribers`,
|
||||
[PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS]: `${RouteParams.ModelID}/sms-subscribers`,
|
||||
[PageMap.STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS]: `${RouteParams.ModelID}/slack-subscribers`,
|
||||
[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]: `${RouteParams.ModelID}/microsoft-teams-subscribers`,
|
||||
[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]: `${RouteParams.ModelID}/webhook-subscribers`,
|
||||
[PageMap.STATUS_PAGE_VIEW_HEADER_STYLE]: `${RouteParams.ModelID}/header-style`,
|
||||
[PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE]: `${RouteParams.ModelID}/footer-style`,
|
||||
@@ -1081,6 +1082,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/status-pages/${
|
||||
StatusPagesRoutePath[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/status-pages/${
|
||||
StatusPagesRoutePath[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]
|
||||
|
||||
@@ -92,7 +92,7 @@ The following example shows how to use a JavaScript expression to monitor an inc
|
||||
|
||||
// you can combine multiple expressions using logical operators
|
||||
|
||||
"{{requestBody.item}"} === "hello" && "{{requestHeaders.contentType}}" === "text/html"
|
||||
"{{requestBody.item}}" === "hello" && "{{requestHeaders.contentType}}" === "text/html"
|
||||
|
||||
// you can use the following for arrays
|
||||
|
||||
|
||||
@@ -16,6 +16,37 @@ docker run --name oneuptime-probe --network host -e PROBE_KEY=<probe-key> -e PRO
|
||||
|
||||
If you are self hosting OneUptime, you can change `ONEUPTIME_URL` to your custom self hosted instance.
|
||||
|
||||
##### Proxy Configuration
|
||||
|
||||
If your probe needs to go through a proxy server to reach OneUptime or monitor external resources, you can configure proxy settings using these environment variables:
|
||||
|
||||
```
|
||||
# For HTTP proxy
|
||||
docker run --name oneuptime-probe --network host \
|
||||
-e PROBE_KEY=<probe-key> \
|
||||
-e PROBE_ID=<probe-id> \
|
||||
-e ONEUPTIME_URL=https://oneuptime.com \
|
||||
-e HTTP_PROXY_URL=http://proxy.example.com:8080 \
|
||||
-d oneuptime/probe:release
|
||||
|
||||
# For HTTPS proxy
|
||||
docker run --name oneuptime-probe --network host \
|
||||
-e PROBE_KEY=<probe-key> \
|
||||
-e PROBE_ID=<probe-id> \
|
||||
-e ONEUPTIME_URL=https://oneuptime.com \
|
||||
-e HTTPS_PROXY_URL=http://proxy.example.com:8080 \
|
||||
-d oneuptime/probe:release
|
||||
|
||||
# With proxy authentication
|
||||
docker run --name oneuptime-probe --network host \
|
||||
-e PROBE_KEY=<probe-key> \
|
||||
-e PROBE_ID=<probe-id> \
|
||||
-e ONEUPTIME_URL=https://oneuptime.com \
|
||||
-e HTTP_PROXY_URL=http://username:password@proxy.example.com:8080 \
|
||||
-e HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080 \
|
||||
-d oneuptime/probe:release
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
You can also run the probe using docker-compose. Create a `docker-compose.yml` file with the following content:
|
||||
@@ -35,6 +66,31 @@ services:
|
||||
restart: always
|
||||
```
|
||||
|
||||
##### With Proxy Configuration
|
||||
|
||||
If you need to use a proxy server, you can add proxy environment variables:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
oneuptime-probe:
|
||||
image: oneuptime/probe:release
|
||||
container_name: oneuptime-probe
|
||||
environment:
|
||||
- PROBE_KEY=<probe-key>
|
||||
- PROBE_ID=<probe-id>
|
||||
- ONEUPTIME_URL=https://oneuptime.com
|
||||
# Proxy configuration (optional)
|
||||
- HTTP_PROXY_URL=http://proxy.example.com:8080
|
||||
- HTTPS_PROXY_URL=http://proxy.example.com:8080
|
||||
# For proxy with authentication:
|
||||
# - HTTP_PROXY_URL=http://username:password@proxy.example.com:8080
|
||||
# - HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080
|
||||
network_mode: host
|
||||
restart: always
|
||||
```
|
||||
|
||||
Then run the following command:
|
||||
|
||||
```
|
||||
@@ -56,20 +112,61 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: oneuptime-probe
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: oneuptime-probe
|
||||
spec:
|
||||
containers:
|
||||
image: oneuptime/probe:release
|
||||
env:
|
||||
- name: PROBE_KEY
|
||||
value: "<probe-key>"
|
||||
- name: PROBE_ID
|
||||
value: "<probe-id>"
|
||||
- name: ONEUPTIME_URL
|
||||
value: "https://oneuptime.com"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: oneuptime-probe
|
||||
spec:
|
||||
containers:
|
||||
- name: oneuptime-probe
|
||||
image: oneuptime/probe:release
|
||||
env:
|
||||
- name: PROBE_KEY
|
||||
value: "<probe-key>"
|
||||
- name: PROBE_ID
|
||||
value: "<probe-id>"
|
||||
- name: ONEUPTIME_URL
|
||||
value: "https://oneuptime.com"
|
||||
```
|
||||
|
||||
##### With Proxy Configuration
|
||||
|
||||
If you need to use a proxy server, you can add proxy environment variables:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: oneuptime-probe
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: oneuptime-probe
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: oneuptime-probe
|
||||
spec:
|
||||
containers:
|
||||
- name: oneuptime-probe
|
||||
image: oneuptime/probe:release
|
||||
env:
|
||||
- name: PROBE_KEY
|
||||
value: "<probe-key>"
|
||||
- name: PROBE_ID
|
||||
value: "<probe-id>"
|
||||
- name: ONEUPTIME_URL
|
||||
value: "https://oneuptime.com"
|
||||
# Proxy configuration (optional)
|
||||
- name: HTTP_PROXY_URL
|
||||
value: "http://proxy.example.com:8080"
|
||||
- name: HTTPS_PROXY_URL
|
||||
value: "http://proxy.example.com:8080"
|
||||
# For proxy with authentication, use:
|
||||
# - name: HTTP_PROXY_URL
|
||||
# value: "http://username:password@proxy.example.com:8080"
|
||||
# - name: HTTPS_PROXY_URL
|
||||
# value: "http://username:password@proxy.example.com:8080"
|
||||
```
|
||||
|
||||
Then run the following command:
|
||||
@@ -80,6 +177,46 @@ kubectl apply -f oneuptime-probe.yaml
|
||||
|
||||
If you are self hosting OneUptime, you can change `ONEUPTIME_URL` to your custom self hosted instance.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The probe supports the following environment variables:
|
||||
|
||||
#### Required Variables
|
||||
- `PROBE_KEY` - The probe key from your OneUptime dashboard
|
||||
- `PROBE_ID` - The probe ID from your OneUptime dashboard
|
||||
- `ONEUPTIME_URL` - The URL of your OneUptime instance (default: https://oneuptime.com)
|
||||
|
||||
#### Optional Variables
|
||||
- `HTTP_PROXY_URL` - HTTP proxy server URL for HTTP requests
|
||||
- `HTTPS_PROXY_URL` - HTTP proxy server URL for HTTPS requests
|
||||
- `PROBE_NAME` - Custom name for the probe
|
||||
- `PROBE_DESCRIPTION` - Description for the probe
|
||||
- `PROBE_MONITORING_WORKERS` - Number of monitoring workers (default: 1)
|
||||
- `PROBE_MONITOR_FETCH_LIMIT` - Number of monitors to fetch at once (default: 10)
|
||||
- `PROBE_MONITOR_RETRY_LIMIT` - Number of retries for failed monitors (default: 3)
|
||||
- `PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS` - Timeout for synthetic monitor scripts in milliseconds (default: 60000)
|
||||
- `PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS` - Timeout for custom code monitor scripts in milliseconds (default: 60000)
|
||||
|
||||
#### Proxy Configuration
|
||||
|
||||
The probe supports both HTTP and HTTPS proxy servers. When configured, the probe will route all monitoring traffic through the specified proxy servers.
|
||||
|
||||
**Proxy URL Format:**
|
||||
```
|
||||
http://[username:password@]proxy.server.com:port
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- Basic proxy: `http://proxy.example.com:8080`
|
||||
- With authentication: `http://username:password@proxy.example.com:8080`
|
||||
|
||||
**Supported Features:**
|
||||
- HTTP and HTTPS proxy support
|
||||
- Proxy authentication (username/password)
|
||||
- Automatic fallback between HTTP and HTTPS proxies
|
||||
- Works with all monitor types (Website, API, SSL, Synthetic, etc.)
|
||||
|
||||
**Note:** Both standard environment variables (`HTTP_PROXY_URL`, `HTTPS_PROXY_URL`) and lowercase variants (`http_proxy`, `https_proxy`) are supported for compatibility.
|
||||
|
||||
### Verify
|
||||
|
||||
|
||||
@@ -7,30 +7,79 @@
|
||||
}) %>
|
||||
</head>
|
||||
|
||||
<body class="flex min-h-full bg-white ">
|
||||
|
||||
<body class="flex min-h-full bg-white">
|
||||
<div class="flex w-full flex-col">
|
||||
<%- include('./Partials/Header.ejs') %>
|
||||
|
||||
<!-- Mobile top bar with menu button -->
|
||||
<div class="flex items-center justify-between lg:hidden border-b border-slate-200 px-4 py-2 gap-2">
|
||||
<button aria-label="Open navigation" data-mobile-menu-open class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-600 shadow-sm active:scale-[.97]">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
Menu
|
||||
</button>
|
||||
<h1 class="text-sm font-semibold text-slate-700 truncate"><%- link.title %></h1>
|
||||
</div>
|
||||
<!-- Mobile nav overlay -->
|
||||
<div data-mobile-menu-overlay class="fixed inset-0 z-40 bg-slate-900/40 lg:hidden hidden"></div>
|
||||
<aside data-mobile-menu-drawer class="fixed inset-y-0 left-0 z-50 w-72 overflow-y-auto bg-white px-6 py-6 shadow-lg ring-1 ring-slate-900/10 lg:hidden transform transition ease-out duration-200 -translate-x-full hidden">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-sm font-semibold text-slate-700">Documentation</span>
|
||||
<button aria-label="Close navigation" data-mobile-menu-close class="rounded-md p-2 text-slate-500 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500"><svg class="h-5 w-5" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
|
||||
</div>
|
||||
<%- include('./Partials/Nav.ejs') %>
|
||||
</aside>
|
||||
<div class="relative mx-auto flex w-full max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||
<div class="hidden lg:relative lg:block lg:flex-none">
|
||||
<div class="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 "></div>
|
||||
<div
|
||||
class="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-gradient-to-t from-slate-800 ">
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 top-28 hidden w-px bg-slate-800 "></div>
|
||||
<div
|
||||
class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
|
||||
<%- include('./Partials/Nav.ejs') %>
|
||||
<div class="absolute inset-y-0 right-0 w-[50vw] bg-slate-50"></div>
|
||||
<div class="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-gradient-to-t from-slate-800"></div>
|
||||
<div class="absolute bottom-0 right-0 top-28 hidden w-px bg-slate-800"></div>
|
||||
<div class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
|
||||
<%- include('./Partials/Nav.ejs') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
|
||||
<div class="min-w-0 max-w-2xl flex-auto px-4 py-8 md:py-12 lg:py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
|
||||
<%- include('./Partials/Content.ejs', { category: category, link: link, content: content }) %>
|
||||
<%- include('./Partials/OpenSourceCommitment.ejs', { githubPath: githubPath }) %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile menu toggle (vanilla JS) -->
|
||||
<script>
|
||||
(function(){
|
||||
const openBtn = document.querySelector('[data-mobile-menu-open]');
|
||||
const closeBtn = document.querySelector('[data-mobile-menu-close]');
|
||||
const overlay = document.querySelector('[data-mobile-menu-overlay]');
|
||||
const drawer = document.querySelector('[data-mobile-menu-drawer]');
|
||||
if(!openBtn || !overlay || !drawer){ return; }
|
||||
function openMenu(){
|
||||
overlay.classList.remove('hidden');
|
||||
drawer.classList.remove('hidden');
|
||||
// trigger slide in
|
||||
requestAnimationFrame(()=>{
|
||||
drawer.classList.remove('-translate-x-full');
|
||||
});
|
||||
document.body.classList.add('overflow-hidden');
|
||||
}
|
||||
function closeMenu(){
|
||||
drawer.classList.add('-translate-x-full');
|
||||
overlay.classList.add('hidden');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
// after transition hide drawer
|
||||
drawer.addEventListener('transitionend', function handler(e){
|
||||
if(e.propertyName === 'transform'){
|
||||
if(drawer.classList.contains('-translate-x-full')){
|
||||
drawer.classList.add('hidden');
|
||||
}
|
||||
drawer.removeEventListener('transitionend', handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
openBtn.addEventListener('click', openMenu);
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
closeBtn && closeBtn.addEventListener('click', closeMenu);
|
||||
// Close on escape
|
||||
document.addEventListener('keydown', (e)=>{ if(e.key==='Escape'){ closeMenu(); }});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2,17 +2,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preload" href="/docs/static/fonts/f1.woff2" as="font" crossorigin="" type="font/woff2">
|
||||
<link rel="preload" href="/docs/static/fonts/f2.woff2" as="font" crossorigin="" type="font/woff2">
|
||||
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin="" data-precedence="next">
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||
<link rel="preconnect" href="https://cdn.tailwindcss.com" crossorigin>
|
||||
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin data-precedence="next">
|
||||
<title>OneUptime Documentation</title>
|
||||
<meta name="description"
|
||||
content="Cache every single thing your app could ever do ahead of time, so your code never even has to run at all.">
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16">
|
||||
<meta name="next-size-adjust">
|
||||
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin="" data-precedence="next" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
<script src="https://cdn.tailwindcss.com" defer></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css" crossorigin>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" onload="hljs.highlightAll();"></script>
|
||||
|
||||
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
|
||||
<!-- Google Tag Manager -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<header
|
||||
class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 sm:px-6 lg:px-8 ">
|
||||
class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-4 sm:py-5 border-b border-slate-200 sm:px-6 lg:px-8">
|
||||
|
||||
<div class="relative flex flex-grow basis-0 items-center"><a aria-label="Home page" href="/">
|
||||
<img class="h-8 w-auto" src="/img/3-transparent.svg" alt="">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="text-base lg:text-sm">
|
||||
<nav class="text-base lg:text-sm" style="width:16rem;flex:0 0 16rem;" aria-label="Documentation navigation">
|
||||
<ul role="list" class="space-y-9">
|
||||
<% for(var i=0; i<nav.length; i++) {%>
|
||||
<li>
|
||||
|
||||
50
E2E/Tests/Home/Sitemap.spec.ts
Normal file
50
E2E/Tests/Home/Sitemap.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BASE_URL, IS_BILLING_ENABLED } from "../../Config";
|
||||
import { Page, expect, test, Response } from "@playwright/test";
|
||||
import URL from "Common/Types/API/URL";
|
||||
|
||||
// Helper to fetch sitemap XML via a normal page navigation.
|
||||
async function fetchSitemap(page: Page): Promise<string> {
|
||||
const response: Response | null = await page.goto(
|
||||
URL.fromString(`${BASE_URL.toString()}sitemap.xml`).toString(),
|
||||
{ waitUntil: "networkidle" },
|
||||
);
|
||||
expect(response, "sitemap.xml should respond").toBeTruthy();
|
||||
expect(response?.status(), "sitemap.xml should return 200").toBe(200);
|
||||
|
||||
// Raw content (Playwright wraps XML in HTML view sometimes); extract text.
|
||||
const body: Awaited<ReturnType<typeof page.$>> = await page.$("body");
|
||||
const xml: string = (await body?.innerText()) || (await page.content());
|
||||
return xml;
|
||||
}
|
||||
|
||||
function extractLocs(xml: string): string[] {
|
||||
const regex: RegExp = /<loc>(.*?)<\/loc>/g;
|
||||
const locs: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = regex.exec(xml)) !== null) {
|
||||
if (match[1]) {
|
||||
locs.push(match[1]);
|
||||
}
|
||||
}
|
||||
return locs;
|
||||
}
|
||||
|
||||
test.describe("Home: Sitemap", () => {
|
||||
test("sitemap loads and has home first", async ({ page }: { page: Page }) => {
|
||||
if (!IS_BILLING_ENABLED) {
|
||||
return; // mirror existing pattern
|
||||
}
|
||||
|
||||
page.setDefaultNavigationTimeout(120000);
|
||||
|
||||
const xml: string = await fetchSitemap(page);
|
||||
expect(xml.includes("<urlset")).toBeTruthy();
|
||||
|
||||
const locs: string[] = extractLocs(xml);
|
||||
expect(locs.length).toBeGreaterThan(0);
|
||||
|
||||
const first: string | undefined = locs[0];
|
||||
expect(first, "First <loc> should exist").toBeTruthy();
|
||||
expect(first!.endsWith("/")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
logger.info(`FluentIngest Service - Queue concurrency: ${FLUENT_INGEST_CONCURRENCY}`);
|
||||
logger.info(
|
||||
`FluentIngest Service - Queue concurrency: ${FLUENT_INGEST_CONCURRENCY}`,
|
||||
);
|
||||
|
||||
// init the app
|
||||
await App.init({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -93,9 +93,40 @@ app.get(
|
||||
async (req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const tagName: string = req.params["tagName"] as string;
|
||||
const tagSlug: string = tagName; // original slug
|
||||
|
||||
const blogPosts: Array<BlogPostHeader> =
|
||||
// Pagination params
|
||||
const pageParam: string | undefined = req.query["page"] as
|
||||
| string
|
||||
| undefined;
|
||||
const pageSizeParam: string | undefined = req.query["pageSize"] as
|
||||
| string
|
||||
| undefined;
|
||||
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
|
||||
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
|
||||
if (isNaN(page) || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (isNaN(pageSize) || pageSize < 1) {
|
||||
pageSize = 24;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
const allPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList(tagName);
|
||||
const totalPosts: number = allPosts.length;
|
||||
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
|
||||
if (page > totalPages) {
|
||||
page = totalPages;
|
||||
}
|
||||
const start: number = (page - 1) * pageSize;
|
||||
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
|
||||
start,
|
||||
start + pageSize,
|
||||
);
|
||||
const allTags: Array<string> = await BlogPostUtil.getTags();
|
||||
|
||||
res.render(`${ViewsPath}/Blog/ListByTag`, {
|
||||
support: false,
|
||||
@@ -103,8 +134,15 @@ app.get(
|
||||
cta: true,
|
||||
blackLogo: false,
|
||||
requestDemoCta: false,
|
||||
blogPosts: blogPosts,
|
||||
blogPosts: paginatedPosts,
|
||||
tagName: Text.fromDashesToPascalCase(tagName),
|
||||
tagSlug: tagSlug,
|
||||
allTags: allTags,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
totalPages: totalPages,
|
||||
totalPosts: totalPosts,
|
||||
basePath: `/blog/tag/${tagSlug}`,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -117,8 +155,38 @@ app.get(
|
||||
// main blog page
|
||||
app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const blogPosts: Array<BlogPostHeader> =
|
||||
const req: ExpressRequest = _req; // alias for clarity
|
||||
const pageParam: string | undefined = req.query["page"] as
|
||||
| string
|
||||
| undefined;
|
||||
const pageSizeParam: string | undefined = req.query["pageSize"] as
|
||||
| string
|
||||
| undefined;
|
||||
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
|
||||
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
|
||||
if (isNaN(page) || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (isNaN(pageSize) || pageSize < 1) {
|
||||
pageSize = 24;
|
||||
}
|
||||
if (pageSize > 100) {
|
||||
pageSize = 100;
|
||||
}
|
||||
|
||||
const allPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList();
|
||||
const totalPosts: number = allPosts.length;
|
||||
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
|
||||
if (page > totalPages) {
|
||||
page = totalPages;
|
||||
}
|
||||
const start: number = (page - 1) * pageSize;
|
||||
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
|
||||
start,
|
||||
start + pageSize,
|
||||
);
|
||||
const allTags: Array<string> = await BlogPostUtil.getTags();
|
||||
|
||||
res.render(`${ViewsPath}/Blog/List`, {
|
||||
support: false,
|
||||
@@ -126,7 +194,13 @@ app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
cta: true,
|
||||
blackLogo: false,
|
||||
requestDemoCta: false,
|
||||
blogPosts: blogPosts,
|
||||
blogPosts: paginatedPosts,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
totalPages: totalPages,
|
||||
totalPosts: totalPosts,
|
||||
basePath: `/blog`,
|
||||
allTags: allTags,
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -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 DatabaseConfig from "Common/Server/DatabaseConfig";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
@@ -25,12 +26,41 @@ import Reviews from "./Utils/Reviews";
|
||||
// import jobs.
|
||||
import "./Jobs/UpdateBlog";
|
||||
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
|
||||
import LocalCache from "Common/Server/Infrastructure/LocalCache";
|
||||
|
||||
const HomeFeatureSet: FeatureSet = {
|
||||
init: async (): Promise<void> => {
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
|
||||
//Routes
|
||||
// Middleware to inject baseUrl for templates (used for canonical links)
|
||||
app.use(
|
||||
async (_req: ExpressRequest, res: ExpressResponse, next: () => void) => {
|
||||
if (!res.locals["homeUrl"]) {
|
||||
try {
|
||||
// Try to get cached home URL first.
|
||||
let homeUrl: string | undefined = LocalCache.getString(
|
||||
"home",
|
||||
"url",
|
||||
);
|
||||
|
||||
if (!homeUrl) {
|
||||
homeUrl = (await DatabaseConfig.getHomeUrl())
|
||||
.toString()
|
||||
.replace(/\/$/, "");
|
||||
LocalCache.setString("home", "url", homeUrl);
|
||||
}
|
||||
|
||||
res.locals["homeUrl"] = homeUrl;
|
||||
} catch {
|
||||
// Fallback hard-coded production domain if env misconfigured
|
||||
res.locals["homeUrl"] = "https://oneuptime.com";
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const { reviewsList1, reviewsList2, reviewsList3 } = Reviews;
|
||||
|
||||
@@ -1331,18 +1361,21 @@ const HomeFeatureSet: FeatureSet = {
|
||||
);
|
||||
|
||||
// Dynamic Sitemap
|
||||
app.get("/sitemap.xml", async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const xml: string = await generateSitemapXml();
|
||||
res.setHeader("Content-Type", "text/xml");
|
||||
res.send(xml);
|
||||
} catch (err) {
|
||||
// Fallback minimal static sitemap if dynamic generation fails
|
||||
const fallback: string = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://oneuptime.com/</loc></url>\n</urlset>`;
|
||||
res.setHeader("Content-Type", "text/xml");
|
||||
res.status(200).send(fallback);
|
||||
}
|
||||
});
|
||||
app.get(
|
||||
"/sitemap.xml",
|
||||
async (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const xml: string = await generateSitemapXml();
|
||||
res.setHeader("Content-Type", "text/xml");
|
||||
res.send(xml);
|
||||
} catch {
|
||||
// Fallback minimal static sitemap if dynamic generation fails
|
||||
const fallback: string = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://oneuptime.com/</loc></url>\n</urlset>`;
|
||||
res.setHeader("Content-Type", "text/xml");
|
||||
res.status(200).send(fallback);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* Cache policy for static contents
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONArray, JSONObject, JSONObjectOrArray } from "Common/Types/JSON";
|
||||
import { JSONArray, JSONObject } from "Common/Types/JSON";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import Text from "Common/Types/Text";
|
||||
import API from "Common/Utils/API";
|
||||
import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown";
|
||||
import { BlogRootPath } from "./Config";
|
||||
import LocalFile from "Common/Server/Utils/LocalFile";
|
||||
@@ -19,6 +14,7 @@ export interface BlogPostAuthor {
|
||||
githubUrl: string;
|
||||
profileImageUrl: string;
|
||||
name: string;
|
||||
bio?: string | undefined; // optional bio from Authors.json
|
||||
}
|
||||
|
||||
export interface BlogPostBaseProps {
|
||||
@@ -44,6 +40,45 @@ export interface BlogPost extends BlogPostBaseProps {
|
||||
}
|
||||
|
||||
export default class BlogPostUtil {
|
||||
// Cache Blogs.json contents to avoid repeated disk reads and external calls.
|
||||
private static blogsMetaCache: Array<JSONObject> | null = null;
|
||||
// Cache Authors.json (keyed by github username)
|
||||
private static authorsMetaCache: JSONObject | null = null;
|
||||
private static async getBlogsMeta(): Promise<Array<JSONObject>> {
|
||||
if (this.blogsMetaCache) {
|
||||
return this.blogsMetaCache;
|
||||
}
|
||||
|
||||
const filePath: string = `${BlogRootPath}/Blogs.json`;
|
||||
let jsonContent: string | JSONArray = await LocalFile.read(filePath);
|
||||
if (typeof jsonContent === "string") {
|
||||
jsonContent = JSONFunctions.parseJSONArray(jsonContent);
|
||||
}
|
||||
const blogs: Array<JSONObject> = JSONFunctions.deserializeArray(
|
||||
jsonContent as Array<JSONObject>,
|
||||
);
|
||||
this.blogsMetaCache = blogs;
|
||||
return blogs;
|
||||
}
|
||||
|
||||
private static async getAuthorsMeta(): Promise<JSONObject> {
|
||||
if (this.authorsMetaCache) {
|
||||
return this.authorsMetaCache;
|
||||
}
|
||||
const filePath: string = `${BlogRootPath}/Authors.json`;
|
||||
try {
|
||||
let jsonContent: string | JSONObject = await LocalFile.read(filePath);
|
||||
if (typeof jsonContent === "string") {
|
||||
jsonContent = JSONFunctions.parse(jsonContent) as JSONObject;
|
||||
}
|
||||
this.authorsMetaCache = jsonContent as JSONObject;
|
||||
return this.authorsMetaCache || ({} as JSONObject);
|
||||
} catch {
|
||||
this.authorsMetaCache = {} as JSONObject;
|
||||
return this.authorsMetaCache;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getBlogPostList(
|
||||
tagName?: string | undefined,
|
||||
): Promise<BlogPostHeader[]> {
|
||||
@@ -98,30 +133,6 @@ export default class BlogPostUtil {
|
||||
return blogPost;
|
||||
}
|
||||
|
||||
public static async getNameOfGitHubUser(username: string): Promise<string> {
|
||||
const fileUrl: URL = URL.fromString(
|
||||
`https://api.github.com/users/${username}`,
|
||||
);
|
||||
|
||||
const fileData:
|
||||
| HTTPResponse<
|
||||
| JSONObjectOrArray
|
||||
| BaseModel
|
||||
| BaseModel[]
|
||||
| AnalyticsBaseModel
|
||||
| AnalyticsBaseModel[]
|
||||
>
|
||||
| HTTPErrorResponse = await API.get(fileUrl);
|
||||
|
||||
if (fileData.isFailure()) {
|
||||
throw fileData as HTTPErrorResponse;
|
||||
}
|
||||
|
||||
const name: string =
|
||||
(fileData.data as JSONObject)?.["name"]?.toString() || "";
|
||||
return name;
|
||||
}
|
||||
|
||||
public static async getTags(): Promise<string[]> {
|
||||
// check if tags are in cache
|
||||
|
||||
@@ -168,8 +179,43 @@ export default class BlogPostUtil {
|
||||
|
||||
let markdownContent: string = await LocalFile.read(filePath);
|
||||
|
||||
const blogPostAuthor: BlogPostAuthor | null =
|
||||
await this.getAuthorFromFileContent(markdownContent);
|
||||
// Resolve author WITHOUT hitting GitHub API. Use Blogs.json to get username, Authors.json for name/bio.
|
||||
let blogPostAuthor: BlogPostAuthor | null = null;
|
||||
try {
|
||||
const blogsMeta: Array<JSONObject> = await this.getBlogsMeta();
|
||||
const blogMeta: JSONObject | undefined = blogsMeta.find(
|
||||
(b: JSONObject) => {
|
||||
return (b["post"] as string) === fileName;
|
||||
},
|
||||
);
|
||||
const username: string | undefined = blogMeta?.[
|
||||
"authorGitHubUsername"
|
||||
] as string | undefined;
|
||||
if (username) {
|
||||
const authorsMeta: JSONObject = await this.getAuthorsMeta();
|
||||
const authorMeta: JSONObject | undefined = authorsMeta[username] as
|
||||
| JSONObject
|
||||
| undefined;
|
||||
const authorName: string | undefined =
|
||||
(authorMeta?.["authorName"] as string) || undefined;
|
||||
const authorBio: string | undefined =
|
||||
(authorMeta?.["authorBio"] as string) || undefined;
|
||||
blogPostAuthor = {
|
||||
username,
|
||||
githubUrl: `https://github.com/${username}`,
|
||||
profileImageUrl: `https://avatars.githubusercontent.com/${username}`,
|
||||
name: authorName || username,
|
||||
bio: authorBio,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore and fallback
|
||||
}
|
||||
|
||||
// Fallback to parsing markdown (no network) if metadata missing.
|
||||
if (!blogPostAuthor) {
|
||||
blogPostAuthor = await this.getAuthorFromFileContent(markdownContent);
|
||||
}
|
||||
|
||||
const title: string = this.getTitleFromFileContent(markdownContent);
|
||||
const description: string =
|
||||
@@ -346,7 +392,8 @@ export default class BlogPostUtil {
|
||||
username: authorUsername,
|
||||
githubUrl: authorGitHubUrl,
|
||||
profileImageUrl: authorProfileImageUrl,
|
||||
name: await this.getNameOfGitHubUser(authorUsername),
|
||||
// Do NOT call GitHub; use username as name placeholder.
|
||||
name: authorUsername,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,130 +15,171 @@ interface CachedSitemap {
|
||||
const TTL_MS: number = 10 * 60 * 1000;
|
||||
let cache: CachedSitemap | null = null;
|
||||
|
||||
export const generateSitemapXml = async (): Promise<string> => {
|
||||
const now: number = OneUptimeDate.getCurrentDate().getTime();
|
||||
if (cache && now - cache.generatedAt < TTL_MS) {
|
||||
return cache.xml;
|
||||
}
|
||||
export const generateSitemapXml: () => Promise<string> =
|
||||
async (): Promise<string> => {
|
||||
const now: number = OneUptimeDate.getCurrentDate().getTime();
|
||||
if (cache && now - cache.generatedAt < TTL_MS) {
|
||||
return cache.xml;
|
||||
}
|
||||
|
||||
const baseUrl: URL = await BlogPostUtil.getHomeUrl();
|
||||
const baseUrl: URL = await BlogPostUtil.getHomeUrl();
|
||||
|
||||
// Discover static (non-parameterized) routes from Express stack
|
||||
const discoveredStaticPaths: Set<string> = new Set();
|
||||
try {
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
const stack: any[] = (app as any)?._router?.stack || [];
|
||||
for (const layer of stack) {
|
||||
if (!layer) { continue; }
|
||||
const route = layer.route || layer?.handle?.route;
|
||||
if (!route) { continue; }
|
||||
// Only include GET handlers
|
||||
const methods: any = route.methods || {};
|
||||
if (!methods.get) { continue; }
|
||||
const path: string | string[] | undefined = route.path || route?.route?.path;
|
||||
const rawPaths: Array<string | undefined> = Array.isArray(path) ? path : [path];
|
||||
const paths: string[] = rawPaths.filter((p): p is string => !!p);
|
||||
for (let p of paths) {
|
||||
if (!p || typeof p !== "string") { continue; }
|
||||
// Filters: skip parameterized, wildcard, api or file-serving or sitemap itself
|
||||
if (p.includes(":") || p.includes("*") || p.includes("sitemap")) { continue; }
|
||||
// Exclude script or installer endpoints
|
||||
if (p.endsWith(".sh")) { continue; }
|
||||
if (p.startsWith("/api") || p.startsWith("/blog/post")) { continue; }
|
||||
// We'll add compare pages separately with real slugs; skip base compare param route
|
||||
if (p.startsWith("/compare")) { continue; }
|
||||
// Normalize slash
|
||||
if (!p.startsWith("/")) { p = `/${p}`; }
|
||||
discoveredStaticPaths.add(p);
|
||||
// Discover static (non-parameterized) routes from Express stack
|
||||
const discoveredStaticPaths: Set<string> = new Set();
|
||||
try {
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
const stack: any[] = (app as any)?._router?.stack || [];
|
||||
for (const layer of stack) {
|
||||
if (!layer) {
|
||||
continue;
|
||||
}
|
||||
const route: any = layer.route || layer?.handle?.route;
|
||||
if (!route) {
|
||||
continue;
|
||||
}
|
||||
// Only include GET handlers
|
||||
const methods: any = route.methods || {};
|
||||
if (!methods.get) {
|
||||
continue;
|
||||
}
|
||||
const path: string | string[] | undefined =
|
||||
route.path || route?.route?.path;
|
||||
const rawPaths: Array<string | undefined> = Array.isArray(path)
|
||||
? path
|
||||
: [path];
|
||||
const paths: string[] = rawPaths.filter(
|
||||
(p: string | undefined): p is string => {
|
||||
return Boolean(p);
|
||||
},
|
||||
);
|
||||
for (let p of paths) {
|
||||
if (!p || typeof p !== "string") {
|
||||
continue;
|
||||
}
|
||||
// Filters: skip parameterized, wildcard, api or file-serving or sitemap itself
|
||||
if (p.includes(":") || p.includes("*") || p.includes("sitemap")) {
|
||||
continue;
|
||||
}
|
||||
// Exclude script or installer endpoints
|
||||
if (p.endsWith(".sh")) {
|
||||
continue;
|
||||
}
|
||||
if (p.startsWith("/api") || p.startsWith("/blog/post")) {
|
||||
continue;
|
||||
}
|
||||
// We'll add compare pages separately with real slugs; skip base compare param route
|
||||
if (p.startsWith("/compare")) {
|
||||
continue;
|
||||
}
|
||||
// Normalize slash
|
||||
if (!p.startsWith("/")) {
|
||||
p = `/${p}`;
|
||||
}
|
||||
discoveredStaticPaths.add(p);
|
||||
}
|
||||
}
|
||||
// Ensure root present
|
||||
discoveredStaticPaths.add("/");
|
||||
// Ensure docs main landing page present (may be served statically and not discoverable)
|
||||
discoveredStaticPaths.add("/docs");
|
||||
// add /reference
|
||||
discoveredStaticPaths.add("/reference");
|
||||
} catch {
|
||||
// If introspection fails, fall back to minimal set
|
||||
discoveredStaticPaths.add("/");
|
||||
discoveredStaticPaths.add("/blog");
|
||||
}
|
||||
// Ensure root present
|
||||
discoveredStaticPaths.add("/");
|
||||
} catch {
|
||||
// If introspection fails, fall back to minimal set
|
||||
discoveredStaticPaths.add("/");
|
||||
discoveredStaticPaths.add("/blog");
|
||||
}
|
||||
|
||||
const staticPaths: string[] = Array.from(discoveredStaticPaths);
|
||||
const staticPaths: string[] = Array.from(discoveredStaticPaths);
|
||||
|
||||
// Product compare pages
|
||||
const productComparePaths: string[] = getProductCompareSlugs().map(
|
||||
(slug: string) => `/compare/${slug}`,
|
||||
);
|
||||
// Product compare pages
|
||||
const productComparePaths: string[] = getProductCompareSlugs().map(
|
||||
(slug: string) => {
|
||||
return `/compare/${slug}`;
|
||||
},
|
||||
);
|
||||
|
||||
// Blog posts
|
||||
const blogPosts: Array<BlogPostHeader> = await BlogPostUtil.getBlogPostList();
|
||||
const blogPostEntries = 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
|
||||
: `${baseUrl.toString()}${post.blogUrl.replace(/^\//, "")}`;
|
||||
return {
|
||||
loc,
|
||||
lastmod: new Date(post.postDate).toISOString(),
|
||||
};
|
||||
});
|
||||
// Blog posts
|
||||
const blogPosts: Array<BlogPostHeader> =
|
||||
await BlogPostUtil.getBlogPostList();
|
||||
const blogPostEntries: any[] = 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
|
||||
: `${baseUrl.toString()}${post.blogUrl.replace(/^\//, "")}`;
|
||||
return {
|
||||
loc,
|
||||
lastmod: new Date(post.postDate).toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
// Blog tags
|
||||
const tags: string[] = await BlogPostUtil.getTags();
|
||||
const tagEntries = tags.map((tag: string) => {
|
||||
const tagSlug: string = tag
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.trim();
|
||||
return {
|
||||
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
|
||||
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
|
||||
};
|
||||
});
|
||||
// Blog tags
|
||||
const tags: string[] = await BlogPostUtil.getTags();
|
||||
const tagEntries: any[] = tags.map((tag: string) => {
|
||||
const tagSlug: string = tag.toLowerCase().replace(/\s+/g, "-").trim();
|
||||
return {
|
||||
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
|
||||
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const timestamp: string = OneUptimeDate.getCurrentDate().toISOString();
|
||||
const timestamp: string = OneUptimeDate.getCurrentDate().toISOString();
|
||||
|
||||
interface Entry { loc: string; lastmod: string }
|
||||
const entries: Entry[] = [
|
||||
...staticPaths.map((p: string) => ({
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
})),
|
||||
...productComparePaths.map((p: string) => ({
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
})),
|
||||
...blogPostEntries,
|
||||
...tagEntries,
|
||||
];
|
||||
|
||||
// Remove duplicates (possible if overlap)
|
||||
const dedupMap: Map<string, Entry> = new Map();
|
||||
entries.forEach((e: Entry) => {
|
||||
if (!dedupMap.has(e.loc)) {
|
||||
dedupMap.set(e.loc, e);
|
||||
interface Entry {
|
||||
loc: string;
|
||||
lastmod: string;
|
||||
}
|
||||
});
|
||||
const entries: Entry[] = [
|
||||
...staticPaths.map((p: string) => {
|
||||
return {
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
};
|
||||
}),
|
||||
...productComparePaths.map((p: string) => {
|
||||
return {
|
||||
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
|
||||
lastmod: timestamp,
|
||||
};
|
||||
}),
|
||||
...blogPostEntries,
|
||||
...tagEntries,
|
||||
];
|
||||
|
||||
const urlset: XMLBuilder = create().ele("urlset");
|
||||
urlset.att("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
|
||||
// Remove duplicates (possible if overlap)
|
||||
const dedupMap: Map<string, Entry> = new Map();
|
||||
entries.forEach((e: Entry) => {
|
||||
if (!dedupMap.has(e.loc)) {
|
||||
dedupMap.set(e.loc, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure home URL is first
|
||||
const baseUrlString: string = baseUrl.toString();
|
||||
const orderedEntries = Array.from(dedupMap.values());
|
||||
orderedEntries.sort((a, b) => {
|
||||
if (a.loc === baseUrlString) { return -1; }
|
||||
if (b.loc === baseUrlString) { return 1; }
|
||||
return 0; // preserve relative order otherwise
|
||||
});
|
||||
const urlset: XMLBuilder = create().ele("urlset");
|
||||
urlset.att("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
|
||||
|
||||
for (const entry of orderedEntries) {
|
||||
const urlEle: XMLBuilder = urlset.ele("url");
|
||||
urlEle.ele("loc").txt(entry.loc);
|
||||
urlEle.ele("lastmod").txt(entry.lastmod);
|
||||
}
|
||||
// Ensure home URL is first
|
||||
const baseUrlString: string = baseUrl.toString();
|
||||
const orderedEntries: any[] = Array.from(dedupMap.values());
|
||||
orderedEntries.sort((a: any, b: any) => {
|
||||
if (a.loc === baseUrlString) {
|
||||
return -1;
|
||||
}
|
||||
if (b.loc === baseUrlString) {
|
||||
return 1;
|
||||
}
|
||||
return 0; // preserve relative order otherwise
|
||||
});
|
||||
|
||||
const xml: string = urlset.end({ prettyPrint: true });
|
||||
for (const entry of orderedEntries) {
|
||||
const urlEle: XMLBuilder = urlset.ele("url");
|
||||
urlEle.ele("loc").txt(entry.loc);
|
||||
urlEle.ele("lastmod").txt(entry.lastmod);
|
||||
}
|
||||
|
||||
cache = { xml, generatedAt: now };
|
||||
return xml;
|
||||
};
|
||||
const xml: string = urlset.end({ prettyPrint: true });
|
||||
|
||||
cache = { xml, generatedAt: now };
|
||||
return xml;
|
||||
};
|
||||
|
||||
export default generateSitemapXml;
|
||||
|
||||
@@ -20,31 +20,55 @@
|
||||
<div class="relative isolate overflow-hidden bg-white">
|
||||
<div class="py-24 sm:py-32">
|
||||
|
||||
<%- include('./Partials/BlogTitleAndDescription', { title: 'Engineering Uptime', smallTitle: '- Blog by OneUptime', description: 'Latest posts on Observability, Monitoring, Reliability and more.' }) -%>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
<div>
|
||||
<%- include('./Partials/BlogTitleAndDescription', { title: 'Engineering Uptime', smallTitle: '- Blog by OneUptime', description: 'Latest posts on Observability, Monitoring, Reliability and more.' }) -%>
|
||||
|
||||
<div class="mt-4">
|
||||
<% if(blogPosts.length> 0){ %>
|
||||
<ul role="list" class="divide-y divide-gray-100 list-none">
|
||||
|
||||
<% for(var i=0; i<blogPosts.length; i++) {%>
|
||||
|
||||
|
||||
|
||||
<%- include('./Partials/ListItem', { blogPost: blogPosts[i] }) -%>
|
||||
|
||||
<% } %>
|
||||
|
||||
</ul>
|
||||
<% } %>
|
||||
<% const featured = blogPosts[0]; const rest = blogPosts.slice(1); %>
|
||||
<!-- Featured Post -->
|
||||
<div class="relative mb-14 rounded-3xl overflow-hidden border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-indigo-100/40 p-8 md:p-12 shadow-sm ring-1 ring-indigo-100/60">
|
||||
<div class="grid md:grid-cols-5 gap-10 items-center">
|
||||
<div class="md:col-span-3">
|
||||
<a href="<%- featured.blogUrl %>" class="group no-underline">
|
||||
<p class="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-indigo-700 bg-indigo-100/70 px-3 py-1 rounded-full ring-1 ring-indigo-200">Featured</p>
|
||||
<h2 class="mt-5 text-3xl md:text-4xl font-bold tracking-tight text-gray-900 group-hover:text-indigo-700 transition-colors"><%- featured.title %></h2>
|
||||
<p class="mt-5 text-base md:text-lg leading-relaxed text-gray-600 line-clamp-5"><%- featured.description %></p>
|
||||
<div class="mt-6 text-xs text-gray-500 flex flex-wrap items-center gap-2">
|
||||
<img class="h-8 w-8 rounded-full ring-2 ring-white shadow" src="https://avatars.githubusercontent.com/<%- featured.authorGitHubUsername -%>?s=80" alt="@<%- featured.authorGitHubUsername -%>" width="32" height="32" loading="lazy" decoding="async">
|
||||
<span>@<%- featured.authorGitHubUsername -%></span>
|
||||
<span class="select-none">•</span>
|
||||
<span><%- featured.formattedPostDate -%></span>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<%- include('./Partials/Tags', { blogPost: featured }) -%>
|
||||
</div>
|
||||
<div class="mt-8 inline-flex items-center gap-2 text-sm font-semibold text-indigo-600 group-hover:text-indigo-500">Read article <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="md:col-span-2 relative">
|
||||
<div class="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(99,102,241,0.15),transparent_70%)] pointer-events-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts Grid -->
|
||||
<div id="blog-posts-grid" class="grid grid-cols-1 gap-10 sm:grid-cols-2 xl:grid-cols-3 items-stretch">
|
||||
<% for(var i=0; i<rest.length; i++) { const post = rest[i]; %>
|
||||
<div data-blog-card class="h-full" data-search="<%- (post.title + ' ' + post.description + ' ' + post.tags.join(' ')).toLowerCase() -%>">
|
||||
<%- include('./Partials/ListItem', { blogPost: post }) -%>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div id="no-search-results" class="hidden text-center text-gray-500 text-sm py-16">No posts match your search.</div>
|
||||
<%- include('./Partials/Pagination', { page: page, pageSize: pageSize, totalPages: totalPages, totalPosts: totalPosts, basePath: basePath }) -%>
|
||||
<% } else { %>
|
||||
<div class="text-center text-gray-600 text-lg py-12">No blog posts found.</div>
|
||||
<% } %>
|
||||
<div class="mt-24">
|
||||
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,30 +20,23 @@
|
||||
<div class="relative isolate overflow-hidden bg-white">
|
||||
<div class="py-24 sm:py-32">
|
||||
|
||||
<%- include('./Partials/BlogTitleAndDescription', { title: 'Latest posts on '+tagName, smallTitle: "", description: 'Here are some of the latest posts on '+tagName }) -%>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
<div>
|
||||
|
||||
<%- include('./Partials/BlogTitleAndDescription', { title: 'Latest posts on '+tagName, smallTitle: "", description: 'Here are some of the latest posts on '+tagName }) -%>
|
||||
<div class="mt-16">
|
||||
<% if(blogPosts.length> 0){ %>
|
||||
<ul role="list" class="divide-y divide-gray-100 list-none">
|
||||
|
||||
<% for(var i=0; i<blogPosts.length; i++) {%>
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<% for(var i=0; i<blogPosts.length; i++) { %>
|
||||
<%- include('./Partials/ListItem', { blogPost: blogPosts[i] }) -%>
|
||||
|
||||
<% } %>
|
||||
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% } %>
|
||||
</div>
|
||||
<%- include('./Partials/Pagination', { page: page, pageSize: pageSize, totalPages: totalPages, totalPosts: totalPosts, basePath: basePath }) -%>
|
||||
<% } else { %>
|
||||
<div class="text-center text-gray-600 text-lg py-12">No posts found for this tag.</div>
|
||||
<% } %>
|
||||
<div class="mt-20">
|
||||
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
78
Home/Views/Blog/Partials/AllTagsFilter.ejs
Normal file
78
Home/Views/Blog/Partials/AllTagsFilter.ejs
Normal file
@@ -0,0 +1,78 @@
|
||||
<% if(allTags && allTags.length){ %>
|
||||
<div class="mb-14" id="tag-filter-root">
|
||||
<div class="mb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-semibold text-gray-700 flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 7h.01M4 6a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0012.414 6H18a2 2 0 012 2v2M4 8v8a2 2 0 002 2h2m12-8v4a2 2 0 01-2 2h-4m-6 0h6m-6 0a2 2 0 01-2-2v-2"/></svg> Tags</span>
|
||||
<span class="hidden md:inline text-xs rounded-full bg-indigo-50 text-indigo-700 px-2 py-0.5 font-medium"> <%- allTags.length %> </span>
|
||||
</div>
|
||||
<div class="relative w-full md:w-72">
|
||||
<input id="tag-filter-search" type="text" placeholder="Search tags..." class="w-full rounded-md border border-gray-200 bg-white py-2 pl-9 pr-3 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" aria-label="Search tags" />
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div id="tag-filter-container" class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin snap-x snap-mandatory" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||
<style>
|
||||
#tag-filter-container::-webkit-scrollbar{display:none;}
|
||||
</style>
|
||||
<a href="/blog" data-tag="all" class="tag-chip <%= !tagName ? 'tag-chip-active' : '' %>" aria-label="Show all posts">All</a>
|
||||
<% for(var i=0;i<allTags.length;i++){ const t = allTags[i]; const slug = t.replaceAll(' ','-').toLowerCase(); const active = (tagName && tagName.toLowerCase()===t.toLowerCase()); %>
|
||||
<a href="/blog/tag/<%- slug -%>" data-tag="<%- slug %>" class="tag-chip <%= active ? 'tag-chip-active' : '' %>" aria-label="Filter by <%- t %>"><%- t %></a>
|
||||
<% } %>
|
||||
<div id="no-tag-results" class="hidden px-3 py-1.5 text-xs text-gray-500">No tags match.</div>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute top-0 right-0 h-full w-10 bg-gradient-to-l from-white to-transparent hidden md:block"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between text-[11px] text-gray-500">
|
||||
<div id="tag-count-info">Showing <%- allTags.length %> tags</div>
|
||||
<button id="toggle-more-tags" type="button" class="hidden text-indigo-600 hover:text-indigo-500 font-medium">Show more</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
if(window.__TAG_FILTER_INITIALIZED__) { return; }
|
||||
window.__TAG_FILTER_INITIALIZED__ = true;
|
||||
const searchInput = document.getElementById('tag-filter-search');
|
||||
const container = document.getElementById('tag-filter-container');
|
||||
const info = document.getElementById('tag-count-info');
|
||||
const noResults = document.getElementById('no-tag-results');
|
||||
if(!searchInput || !container) { return; }
|
||||
const chips = Array.from(container.querySelectorAll('.tag-chip'));
|
||||
function normalize(s){ return (s||'').toLowerCase(); }
|
||||
function filter(){
|
||||
const q = normalize(searchInput.value);
|
||||
let visible = 0;
|
||||
chips.forEach(chip => {
|
||||
if(chip.dataset.tag === 'all'){ // always show All
|
||||
chip.classList.remove('hidden');
|
||||
return; }
|
||||
const text = normalize(chip.textContent);
|
||||
if(!q || text.includes(q)) { chip.classList.remove('hidden'); visible++; }
|
||||
else { chip.classList.add('hidden'); }
|
||||
});
|
||||
if(visible === 0 && q){ noResults.classList.remove('hidden'); }
|
||||
else { noResults.classList.add('hidden'); }
|
||||
info && (info.textContent = q ? `Matched ${visible} tag${visible!==1?'s':''}` : `Showing ${chips.length-1} tags`);
|
||||
}
|
||||
searchInput.addEventListener('input', filter);
|
||||
// Keyboard horizontal scroll convenience
|
||||
searchInput.addEventListener('keydown', function(e){
|
||||
if(e.key === 'ArrowRight'){ container.scrollBy({left:120,behavior:'smooth'}); }
|
||||
if(e.key === 'ArrowLeft'){ container.scrollBy({left:-120,behavior:'smooth'}); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Inline fallback styling since Tailwind @apply isn't processed in EJS runtime */
|
||||
.tag-chip { font-size: 0.75rem; font-weight:500; padding:0.375rem 0.75rem; border-radius:9999px; border:1px solid #e5e7eb; background:#ffffff; color:#4b5563; display:inline-flex; align-items:center; transition:all .15s; white-space:nowrap; text-decoration:none; }
|
||||
.tag-chip:hover { color:#4f46e5; border-color:#818cf8; background:#eef2ff; }
|
||||
.tag-chip-active { background:#4f46e5; border-color:#4f46e5; color:#ffffff; }
|
||||
.tag-chip-active:hover { background:#4f46e5; color:#ffffff; }
|
||||
</style>
|
||||
<% } %>
|
||||
@@ -1,17 +1,17 @@
|
||||
<div class="mb-24">
|
||||
<div class="mx-auto flex justify-center text-center ">
|
||||
<h1 class="max-w-5xl text-center text-6xl font-bold text-gray-900 leading-tight">
|
||||
<div class="relative mb-16 md:mb-20">
|
||||
<div class="mx-auto flex justify-center text-center">
|
||||
<div>
|
||||
<h1 class="max-w-5xl text-5xl md:text-6xl font-extrabold tracking-tight text-gray-900 leading-tight bg-clip-text">
|
||||
<%= title %>
|
||||
</h1>
|
||||
</h1>
|
||||
<% if(smallTitle){ %>
|
||||
<h3 class="max-w-5xl mt-4 text-2xl font-semibold text-indigo-600/80 leading-tight">
|
||||
<%= smallTitle %>
|
||||
</h3>
|
||||
<% } %>
|
||||
<p class="max-w-3xl mx-auto text-lg md:text-2xl leading-8 text-gray-600 mt-6">
|
||||
<%= description %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto flex justify-center text-center ">
|
||||
<% if(smallTitle){ %>
|
||||
<h3 class="max-w-5xl mt-5 text-center text-2xl font-medium text-gray-900 leading-tight">
|
||||
<%= smallTitle %>
|
||||
</h3>
|
||||
<% } %>
|
||||
</div>
|
||||
<p class="text-2xl text-center leading-8 text-gray-600 mt-8 leading-normal">
|
||||
<%= description %>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,33 +1,30 @@
|
||||
<li class="py-5">
|
||||
<div class="min-w-0">
|
||||
<a href="<%= blogPost.blogUrl %>">
|
||||
|
||||
<div class="min-w-0 flex-auto">
|
||||
<p class="font-semibold text-2xl leading-6 text-gray-900">
|
||||
<%= blogPost.title %>
|
||||
</p>
|
||||
<p class="mt-2 leading-5 text-gray-600">
|
||||
<%= blogPost.description %>
|
||||
</p>
|
||||
<div class="group relative flex flex-col h-full rounded-2xl border border-gray-200/80 bg-white/70 backdrop-blur-sm p-6 shadow-sm ring-1 ring-gray-200/60 hover:shadow-lg hover:border-indigo-300 hover:ring-indigo-200 transition-all duration-300 focus-within:shadow-lg" tabindex="0" aria-label="Blog post card: <%- blogPost.title.replace(/\"/g,'') -%>">
|
||||
<div class="absolute -inset-px rounded-2xl opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition pointer-events-none bg-gradient-to-br from-indigo-500/5 via-transparent to-indigo-400/10"></div>
|
||||
<div class="flex-1 flex flex-col relative z-10">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<a href="<%= blogPost.blogUrl %>" class="no-underline focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded-md">
|
||||
<h2 class="text-xl font-semibold tracking-tight text-gray-900 group-hover:text-indigo-600 transition-colors line-clamp-2">
|
||||
<%= blogPost.title %>
|
||||
</h2>
|
||||
</a>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 line-clamp-4"><%= blogPost.description %></p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="-mt-5">
|
||||
<div class="mt-4">
|
||||
<%- include('./Tags', { blogPost: blogPost }) -%>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="">
|
||||
|
||||
<a href="https://github.com/<%- blogPost.authorGitHubUsername -%>" target="_blank">
|
||||
<div class="flex items-center gap-x-6">
|
||||
<p class="text-sm font-medium leading-7 tracking-tight text-gray-600">
|
||||
<span class="text-gray-500">By</span> @<%- blogPost.authorGitHubUsername -%> <span
|
||||
class="text-gray-500"> on
|
||||
<%- blogPost.formattedPostDate -%></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<a href="https://github.com/<%- blogPost.authorGitHubUsername -%>" target="_blank" class="flex items-center gap-3 group/author">
|
||||
<img loading="lazy" src="https://avatars.githubusercontent.com/<%- blogPost.authorGitHubUsername -%>?s=64" alt="@<%- blogPost.authorGitHubUsername -%>" class="h-10 w-10 rounded-full ring-2 ring-white shadow-sm group-hover/author:ring-indigo-200 transition" width="40" height="40" decoding="async">
|
||||
<div class="text-[11px] leading-4 text-gray-600">
|
||||
<span class="text-gray-500">By</span> @<%- blogPost.authorGitHubUsername -%><br/>
|
||||
<span class="text-gray-400"><%- blogPost.formattedPostDate -%></span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= blogPost.blogUrl %>" class="shrink-0 text-sm font-medium text-indigo-600 hover:text-indigo-500 inline-flex items-center gap-1 group/cta">Read
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4 transition-transform group-hover/cta:translate-x-0.5"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
37
Home/Views/Blog/Partials/Pagination.ejs
Normal file
37
Home/Views/Blog/Partials/Pagination.ejs
Normal file
@@ -0,0 +1,37 @@
|
||||
<% if(totalPages && totalPages > 1){ %>
|
||||
<nav class="mt-16 flex items-center justify-between" aria-label="Pagination">
|
||||
<div>
|
||||
<% if(page > 1){ %>
|
||||
<a href="<%- basePath %>?page=<%- page-1 %>&pageSize=<%- pageSize %>" class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">Previous</a>
|
||||
<% } else { %>
|
||||
<span class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-200 bg-gray-50 text-gray-300 cursor-not-allowed">Previous</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<% const windowSize = 5; let start = Math.max(1, page - Math.floor(windowSize/2)); let end = Math.min(totalPages, start + windowSize -1); if(end-start+1 < windowSize){ start = Math.max(1, end-windowSize+1);} %>
|
||||
<% if(start > 1){ %>
|
||||
<a href="<%- basePath %>?page=1&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50">1</a>
|
||||
<% if(start > 2){ %><span class="px-2 text-gray-400">…</span><% } %>
|
||||
<% } %>
|
||||
<% for(let p = start; p<=end; p++){ %>
|
||||
<% if(p === page){ %>
|
||||
<span class="px-3 py-2 text-sm font-semibold rounded-md bg-indigo-600 text-white border border-indigo-600"><%- p %></span>
|
||||
<% } else { %>
|
||||
<a href="<%- basePath %>?page=<%- p %>&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50"><%- p %></a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if(end < totalPages){ %>
|
||||
<% if(end < totalPages-1){ %><span class="px-2 text-gray-400">…</span><% } %>
|
||||
<a href="<%- basePath %>?page=<%- totalPages %>&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50"><%- totalPages %></a>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if(page < totalPages){ %>
|
||||
<a href="<%- basePath %>?page=<%- page+1 %>&pageSize=<%- pageSize %>" class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">Next</a>
|
||||
<% } else { %>
|
||||
<span class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-200 bg-gray-50 text-gray-300 cursor-not-allowed">Next</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</nav>
|
||||
<p class="mt-4 text-center text-xs text-gray-500">Showing <%- ((page-1)*pageSize)+1 %> - <%- Math.min(page*pageSize, totalPosts) %> of <%- totalPosts %> posts</p>
|
||||
<% } %>
|
||||
@@ -1,22 +1,12 @@
|
||||
<% if(blogPost.tags.length> 0){ %>
|
||||
<div class="flex mt-10">
|
||||
<div class="space-x-1">
|
||||
<!-- Loop over blogPost.tags and show them here-->
|
||||
<% for(var i=0; i<blogPost.tags.length; i++) {%>
|
||||
<a href="/blog/tag/<%- blogPost.tags[i].replaceAll(' ','-').toLowerCase() -%>">
|
||||
<div
|
||||
class="relative inline-flex items-center rounded-full border border-gray-300 px-3 py-0.5 text-sm">
|
||||
<div class="absolute flex flex-shrink-0 items-center justify-center">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-indigo-500" aria-hidden="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3.5 font-medium text-gray-900">
|
||||
<%- blogPost.tags[i] -%>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% for(var i=0; i<blogPost.tags.length; i++) { %>
|
||||
<a href="/blog/tag/<%- blogPost.tags[i].replaceAll(' ','-').toLowerCase() -%>" class="group/tag">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-700 shadow-sm hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 transition">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-indigo-500 group-hover/tag:bg-indigo-600"></span>
|
||||
<%- blogPost.tags[i] -%>
|
||||
</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
@@ -9,7 +9,7 @@
|
||||
</title>
|
||||
<meta name="description" content="<%= blogPost.description %>">
|
||||
<%- include('../head-basic') -%>
|
||||
<link rel="canonical" href="https://oneuptime.com/blog/post/<%= blogPost.fileName %>" />
|
||||
<link rel="canonical" href="https://oneuptime.com/blog/post/<%= blogPost.fileName %>/view" />
|
||||
<meta property="og:site_name" content="OneUptime | One Complete Observability platform.">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="<%= blogPost.title %>">
|
||||
@@ -63,56 +63,131 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
|
||||
<style>
|
||||
/* Custom styles for code blocks in blog posts */
|
||||
.blog-body pre {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', monospace !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.blog-body pre code {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
line-height: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
||||
/* Ensure highlight.js doesn't override our font settings */
|
||||
.blog-body .hljs {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', monospace !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include('../nav') -%>
|
||||
|
||||
<div class="relative isolate overflow-hidden bg-white">
|
||||
<div class="py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
<%- include('./Partials/BlogTitleAndDescription', { title: blogPost.title , smallTitle: "", description:
|
||||
blogPost.description }) -%>
|
||||
|
||||
|
||||
<div class="blog-body">
|
||||
<%- blogPost.htmlBody -%>
|
||||
</div>
|
||||
|
||||
<div class="relative isolate overflow-hidden bg-gradient-to-b from-white via-white to-indigo-50/40">
|
||||
<!-- Hero / Title Section -->
|
||||
<div class="pt-20 pb-12 sm:pt-28 sm:pb-20">
|
||||
<div class="mx-auto max-w-4xl px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl sm:text-5xl font-extrabold tracking-tight text-gray-900 leading-tight">
|
||||
<%= blogPost.title %>
|
||||
</h1>
|
||||
<p class="mt-6 text-xl text-gray-600 leading-relaxed max-w-3xl mx-auto">
|
||||
<%= blogPost.description %>
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-4 text-sm text-gray-500">
|
||||
<% if(blogPost.author){ %>
|
||||
<a href="<%- blogPost.author.githubUrl -%>" target="_blank" class="flex items-center space-x-2 group">
|
||||
<!-- Added explicit width & height to stabilize layout (CLS) -->
|
||||
<img class="h-8 w-8 rounded-full ring-2 ring-indigo-200 group-hover:ring-indigo-400 transition" src="<%- blogPost.author.profileImageUrl -%>" alt="<%- blogPost.author.name -%>" width="32" height="32" decoding="async">
|
||||
<span class="font-medium text-gray-700 group-hover:text-gray-900 transition">@<%- blogPost.author.username -%></span>
|
||||
</a>
|
||||
<span class="hidden sm:inline select-none">•</span>
|
||||
<span><%- blogPost.formattedPostDate -%></span>
|
||||
<span class="hidden sm:inline select-none">•</span>
|
||||
<% } %>
|
||||
<span id="reading-time" class="inline-flex items-center gap-1">
|
||||
<svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<span class="sr-only">Reading time</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<%- include('./Partials/Tags', { blogPost: blogPost }) -%>
|
||||
|
||||
<div class="bg-white my-10 mt-20">
|
||||
<div class="">
|
||||
<ul role="list" class="">
|
||||
<li>
|
||||
<a href="<%- blogPost.author.githubUrl -%>" target="_blank">
|
||||
<div class="flex items-center gap-x-6">
|
||||
<img class="h-12 w-12 rounded-full"
|
||||
src="<%- blogPost.author.profileImageUrl -%>" alt="">
|
||||
<div class="-ml-4">
|
||||
<h3
|
||||
class="text-base font-medium leading-7 tracking-tight text-gray-600">
|
||||
<span class="text-gray-500">By</span> <%-
|
||||
blogPost.author.name -%> <span class="text-gray-500"> on
|
||||
<%- blogPost.formattedPostDate -%></span>
|
||||
</h3>
|
||||
<p class="text-sm font-medium leading-6 text-indigo-500 -mt-1">
|
||||
@<%- blogPost.author.username -%></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('./Partials/OpenSourceCommitment', { blogPost: blogPost }) -%>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content + Sidebar -->
|
||||
<div class="pb-24">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
||||
<!-- Main content -->
|
||||
<article class="lg:col-span-8 xl:col-span-9">
|
||||
<div class="blog-body prose prose-slate max-w-none prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-semibold lg:prose-headings:scroll-mt-[8.5rem] prose-a:font-semibold prose-a:text-indigo-600 hover:prose-a:text-indigo-500 prose-img:rounded-xl prose-pre:rounded-xl prose-code:text-indigo-600">
|
||||
<%- blogPost.htmlBody -%>
|
||||
</div>
|
||||
|
||||
<!-- Share -->
|
||||
<div class="mt-14 border-t border-gray-200/70 pt-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
||||
<div class="text-sm font-medium text-gray-600">Share this article</div>
|
||||
<div class="flex gap-3">
|
||||
<a title="Share on X" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://twitter.com/intent/tweet?text=<%- encodeURIComponent(blogPost.title) -%>&url=<%- encodeURIComponent(blogPost.blogUrl) -%>">
|
||||
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M17.53 3h3.77l-8.26 9.45L23 21h-6.17l-4.8-6.01L6.4 21H2.62l8.63-9.87L1 3h6.32l4.33 5.41L17.53 3Zm-1.33 15.62h2.09L7.94 4.29H5.71l10.49 14.33Z"/></svg>
|
||||
</a>
|
||||
<a title="Share on LinkedIn" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://www.linkedin.com/sharing/share-offsite/?url=<%- encodeURIComponent(blogPost.blogUrl) -%>">
|
||||
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.049c.476-.9 1.637-1.85 3.37-1.85 3.601 0 4.266 2.37 4.266 5.455v6.286ZM5.337 7.433a2.062 2.062 0 1 1 0-4.124 2.062 2.062 0 0 1 0 4.124ZM7.119 20.452H3.553V9h3.566v11.452ZM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003Z"/></svg>
|
||||
</a>
|
||||
<a title="Discuss on Hacker News" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://news.ycombinator.com/submitlink?u=<%- encodeURIComponent(blogPost.blogUrl) -%>&t=<%- encodeURIComponent(blogPost.title) -%>">
|
||||
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3v18h18V3H3Zm9.774 10.876L17 6h-2.117l-2.742 5.274L9.4 6H7l4.226 7.876V18h2.548v-4.124Z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Author -->
|
||||
<% if(blogPost.author){ %>
|
||||
<div class="mt-16 p-6 rounded-2xl bg-white/60 backdrop-blur border border-gray-200 shadow-sm flex gap-6 items-start">
|
||||
<!-- Added explicit width/height + lazy loading for below-the-fold author bio image -->
|
||||
<img class="h-16 w-16 rounded-full ring-2 ring-indigo-200" src="<%- blogPost.author.profileImageUrl -%>" alt="<%- blogPost.author.name -%>" width="64" height="64" loading="lazy" decoding="async">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900"><%- blogPost.author.name -%></h3>
|
||||
<p class="text-sm text-gray-600 mb-2">@<%- blogPost.author.username -%> • <%- blogPost.formattedPostDate -%> • <span id="reading-time-inline"></span></p>
|
||||
<div class="text-sm text-gray-700"><%- blogPost.author.bio || 'Building reliable software at OneUptime. Follow along for more on observability & reliability.' -%></div>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<a href="<%- blogPost.author.githubUrl -%>" target="_blank" class="text-xs font-medium text-indigo-600 hover:text-indigo-500 inline-flex items-center gap-1">View GitHub
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13 5h6m0 0v6m0-6L10 14"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="mt-20">
|
||||
<%- include('./Partials/OpenSourceCommitment', { blogPost: blogPost }) -%>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="hidden lg:block lg:col-span-4 xl:col-span-3">
|
||||
<div class="sticky top-28 space-y-8">
|
||||
<div class="rounded-2xl border border-gray-200 bg-white/70 backdrop-blur p-6 shadow-sm">
|
||||
<h2 class="text-sm font-semibold tracking-wide text-gray-900 uppercase mb-4">On this page</h2>
|
||||
<!-- Reserve initial space for TOC to reduce layout shift once populated -->
|
||||
<nav id="toc" aria-label="Table of contents" class="text-sm leading-6 space-y-2 text-gray-700 min-h-[3rem]"></nav>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -120,6 +195,76 @@
|
||||
|
||||
<%- include('./Partials/BlogCta') -%>
|
||||
<%- include('../footer') -%>
|
||||
<script>
|
||||
(function(){
|
||||
// Reading time
|
||||
try {
|
||||
const container = document.querySelector('.blog-body');
|
||||
if(container){
|
||||
const text = container.textContent || '';
|
||||
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
||||
const minutes = Math.max(1, Math.round(words / 200));
|
||||
const rt = minutes + ' min read';
|
||||
const el = document.getElementById('reading-time');
|
||||
if(el){ el.insertAdjacentText('beforeend', rt); }
|
||||
const el2 = document.getElementById('reading-time-inline');
|
||||
if(el2){ el2.textContent = rt; }
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// TOC
|
||||
try {
|
||||
const container = document.querySelector('.blog-body');
|
||||
if(!container){ return; }
|
||||
const headings = container.querySelectorAll('h2, h3');
|
||||
if(!headings.length){ return; }
|
||||
const toc = document.getElementById('toc');
|
||||
if(!toc){ return; }
|
||||
headings.forEach(h => {
|
||||
if(!h.id){
|
||||
h.id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = '#' + h.id;
|
||||
a.textContent = h.textContent;
|
||||
a.className = 'block hover:text-indigo-600 transition ' + (h.tagName === 'H3' ? 'pl-4 text-gray-500' : 'font-medium');
|
||||
toc.appendChild(a);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// Stabilize images inside blog body (add width/height if missing & lazy-load non-hero images)
|
||||
try {
|
||||
const imgs = document.querySelectorAll('.blog-body img');
|
||||
imgs.forEach((img, index) => {
|
||||
// If width/height already specified, skip dimension inference
|
||||
if(!img.hasAttribute('width') || !img.hasAttribute('height')){
|
||||
if(img.naturalWidth && img.naturalHeight){
|
||||
img.setAttribute('width', img.naturalWidth);
|
||||
img.setAttribute('height', img.naturalHeight);
|
||||
} else {
|
||||
// If not yet loaded, attach a one-time listener
|
||||
img.addEventListener('load', function handler(){
|
||||
if(!img.hasAttribute('width') && img.naturalWidth){ img.setAttribute('width', img.naturalWidth); }
|
||||
if(!img.hasAttribute('height') && img.naturalHeight){ img.setAttribute('height', img.naturalHeight); }
|
||||
img.removeEventListener('load', handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Lazy-load images after the first one (often top/hero) for better LCP while reducing CLS
|
||||
if(index > 0 && !img.hasAttribute('loading')){
|
||||
img.setAttribute('loading', 'lazy');
|
||||
}
|
||||
if(!img.hasAttribute('decoding')){
|
||||
img.setAttribute('decoding', 'async');
|
||||
}
|
||||
// Ensure display block for centered images without causing late shifts
|
||||
if(!img.className.includes('inline') && !img.className.includes('block')){
|
||||
img.classList.add('block');
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -22,10 +22,10 @@
|
||||
|
||||
<div class="hidden sm:block">
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex justify-center space-x-8" aria-label="Tabs">
|
||||
<nav class="-mb-px flex justify-center space-x-8" aria-label="Product feature tabs" role="tablist">
|
||||
<!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
|
||||
<a onclick="showTab(1)"
|
||||
class="tab-1-button cursor-pointer border-indigo-500 text-indigo-600 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
<button type="button" onclick="showTab('status-pages')" id="tab-status-pages" role="tab" aria-selected="true" aria-controls="panel-status-pages"
|
||||
class="tab-status-pages-button tab-1-button cursor-pointer border-indigo-500 text-indigo-600 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="0" aria-label="Status Pages - communicates service availability to users">
|
||||
<!-- Current: "text-indigo-500", Default: "text-gray-400 group-hover:text-gray-500" -->
|
||||
<svg class="icon-tab-1 text-gray-400 text-indigo-500 -ml-0.5 mr-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
@@ -33,67 +33,60 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
||||
<span>Status Pages</span>
|
||||
</a>
|
||||
<a onclick="showTab(2)"
|
||||
class="tab-2-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
</button>
|
||||
<button type="button" onclick="showTab('monitoring')" id="tab-monitoring" role="tab" aria-selected="false" aria-controls="panel-monitoring"
|
||||
class="tab-monitoring-button tab-2-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Monitoring - uptime, performance and synthetic checks">
|
||||
<svg class="icon-tab-2 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
|
||||
<span>Monitoring</span>
|
||||
</a>
|
||||
<a onclick="showTab(3)"
|
||||
class="tab-3-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium"
|
||||
aria-current="page">
|
||||
</button>
|
||||
<button type="button" onclick="showTab('incidents')" id="tab-incidents" role="tab" aria-selected="false" aria-controls="panel-incidents"
|
||||
class="tab-incidents-button tab-3-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Incidents - detect, respond and learn from outages">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tab-3 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
|
||||
<span>Incidents</span>
|
||||
</a>
|
||||
<a onclick="showTab(4)"
|
||||
class="tab-4-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
</button>
|
||||
<button type="button" onclick="showTab('on-call')" id="tab-on-call" role="tab" aria-selected="false" aria-controls="panel-on-call"
|
||||
class="tab-on-call-button tab-4-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="On Call Alerts - intelligent alert routing and scheduling">
|
||||
<svg class="icon-tab-4 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
|
||||
</svg>
|
||||
|
||||
<span>On Call Alerts</span>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<a onclick="showTab(5)"
|
||||
class="tab-5-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
<button type="button" onclick="showTab('logs')" id="tab-logs" role="tab" aria-selected="false" aria-controls="panel-logs"
|
||||
class="tab-logs-button tab-5-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Logs - centralized log management and analytics">
|
||||
<svg class="icon-tab-5 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" >
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />
|
||||
</svg>
|
||||
|
||||
<span>Logs</span>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<a onclick="showTab(6)"
|
||||
class="tab-6-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
<button type="button" onclick="showTab('apm')" id="tab-apm" role="tab" aria-selected="false" aria-controls="panel-apm"
|
||||
class="tab-apm-button tab-6-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="APM - metrics, traces and error tracking for applications">
|
||||
|
||||
<svg class="icon-tab-6 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
|
||||
</svg>
|
||||
|
||||
<span>APM</span>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<a onclick="showTab(7)"
|
||||
class="tab-7-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
|
||||
<button type="button" onclick="showTab('workflows')" id="tab-workflows" role="tab" aria-selected="false" aria-controls="panel-workflows"
|
||||
class="tab-workflows-button tab-7-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Workflows - automate reliability operations and responses">
|
||||
|
||||
<svg class="icon-tab-7 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
@@ -101,9 +94,8 @@
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 01-1.125-1.125v-3.75zM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-8.25zM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-2.25z" />
|
||||
</svg>
|
||||
|
||||
<span>Workflows</span>
|
||||
</a>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -588,36 +580,97 @@
|
||||
|
||||
|
||||
<script>
|
||||
let currentActiveTab = 1;
|
||||
// Semantic tab slugs in desired order
|
||||
const TAB_ORDER = ['status-pages','monitoring','incidents','on-call','logs','apm','workflows'];
|
||||
let currentActiveTab = 'status-pages';
|
||||
|
||||
|
||||
function normalize(tab){
|
||||
// allow old numeric references for backward compatibility
|
||||
if(typeof tab === 'number' || /^\d+$/.test(tab)){ return TAB_ORDER[Number(tab)-1] || 'status-pages'; }
|
||||
return tab;
|
||||
}
|
||||
|
||||
function hideTab(tab) {
|
||||
tab = normalize(tab);
|
||||
const panel = document.querySelector(`.tab-${TAB_ORDER.indexOf(tab)+1}`) || document.querySelector(`.tab-${tab}`);
|
||||
if (panel) { panel.style.display = 'none'; panel.setAttribute('aria-hidden','true'); }
|
||||
|
||||
document.querySelector(`.tab-${tab}`).style.display = 'none';
|
||||
|
||||
document.querySelector(`.tab-${tab}-button`).classList.remove('border-indigo-500', 'text-indigo-600');
|
||||
document.querySelector(`.tab-${tab}-button`).classList.add('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
|
||||
|
||||
document.querySelector(`.icon-tab-${tab}`).classList.remove('text-indigo-500');
|
||||
document.querySelector(`.icon-tab-${tab}`).classList.add('text-gray-400');
|
||||
const button = document.querySelector(`.tab-${tab}-button`) || document.querySelector(`.tab-${TAB_ORDER.indexOf(tab)+1}-button`);
|
||||
if (button) {
|
||||
button.classList.remove('border-indigo-500', 'text-indigo-600');
|
||||
button.classList.add('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
|
||||
button.setAttribute('aria-selected','false');
|
||||
button.setAttribute('tabindex','-1');
|
||||
}
|
||||
const icon = document.querySelector(`.icon-tab-${TAB_ORDER.indexOf(tab)+1}`) || document.querySelector(`.icon-tab-${tab}`);
|
||||
if (icon) { icon.classList.remove('text-indigo-500'); icon.classList.add('text-gray-400'); }
|
||||
}
|
||||
|
||||
function showTab(tab) {
|
||||
|
||||
tab = normalize(tab);
|
||||
if (tab === currentActiveTab) { return; }
|
||||
hideTab(currentActiveTab);
|
||||
|
||||
document.querySelector(`.tab-${tab}`).style.display = 'block';
|
||||
const numericIndex = TAB_ORDER.indexOf(tab)+1;
|
||||
const panel = document.querySelector(`.tab-${numericIndex}`) || document.querySelector(`.tab-${tab}`);
|
||||
if (panel) { panel.style.display = 'block'; panel.setAttribute('aria-hidden','false'); }
|
||||
|
||||
document.querySelector(`.tab-${tab}-button`).classList.remove('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
|
||||
document.querySelector(`.tab-${tab}-button`).classList.add('border-indigo-500', 'text-indigo-600');
|
||||
const button = document.querySelector(`.tab-${tab}-button`) || document.querySelector(`.tab-${numericIndex}-button`);
|
||||
if (button) {
|
||||
button.classList.remove('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
|
||||
button.classList.add('border-indigo-500', 'text-indigo-600');
|
||||
button.setAttribute('aria-selected','true');
|
||||
button.setAttribute('tabindex','0');
|
||||
button.focus();
|
||||
}
|
||||
|
||||
document.querySelector(`.icon-tab-${tab}`).classList.remove('text-gray-400');
|
||||
document.querySelector(`.icon-tab-${tab}`).classList.add('text-indigo-500');
|
||||
const icon = document.querySelector(`.icon-tab-${numericIndex}`) || document.querySelector(`.icon-tab-${tab}`);
|
||||
if (icon) { icon.classList.remove('text-gray-400'); icon.classList.add('text-indigo-500'); }
|
||||
|
||||
currentActiveTab = tab;
|
||||
|
||||
if (history.replaceState) { history.replaceState(null, '', `#tab-${tab}`); }
|
||||
}
|
||||
|
||||
// Initialize tabs accessibility + allow keyboard navigation (arrow keys)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Mark all panels with role="tabpanel" and associate with buttons
|
||||
TAB_ORDER.forEach((slug, idx) => {
|
||||
const panel = document.querySelector(`.tab-${idx+1}`) || document.querySelector(`.tab-${slug}`);
|
||||
if (panel) {
|
||||
panel.setAttribute('role','tabpanel');
|
||||
panel.setAttribute('id',`panel-${slug}`);
|
||||
panel.setAttribute('aria-labelledby',`tab-${slug}`);
|
||||
if (slug !== currentActiveTab) { panel.style.display='none'; panel.setAttribute('aria-hidden','true'); }
|
||||
else { panel.setAttribute('aria-hidden','false'); }
|
||||
}
|
||||
});
|
||||
|
||||
// Support deep linking via hash like #tab-apm or legacy #tab-6
|
||||
const hash = window.location.hash;
|
||||
if (hash && /^#tab-/.test(hash)) {
|
||||
const raw = hash.replace('#tab-','');
|
||||
showTab(raw);
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
const tabButtons = Array.from(document.querySelectorAll('[role="tab"]'));
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('keydown', e => {
|
||||
const idx = tabButtons.indexOf(e.currentTarget);
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
const next = tabButtons[(idx+1)%tabButtons.length];
|
||||
const ident = next.id.replace('tab-',''); showTab(ident);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prev = tabButtons[(idx-1+tabButtons.length)%tabButtons.length];
|
||||
const ident = prev.id.replace('tab-',''); showTab(ident);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<!-- Preconnect to external image/avatar domains to reduce layout shift & latency -->
|
||||
<link rel="preconnect" href="https://avatars.githubusercontent.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="//avatars.githubusercontent.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
@@ -19,6 +22,8 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
appearance: none; /* standard */
|
||||
-webkit-appearance: none; /* vendor */
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
@@ -32,6 +37,8 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: auto;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*Chrome*/
|
||||
@@ -161,5 +168,5 @@
|
||||
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
|
||||
|
||||
<!-- Canonical and Manifest -->
|
||||
<link rel="canonical" href="/">
|
||||
<% /* Expect a homeUrl variable like https://oneuptime.com passed from server; fallback to production domain */ %>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">Build reliable software.</h1>
|
||||
<h1 class="text-6xl font-bold tracking-tight text-gray-900 sm:text-6xl mt-8 mb-8">Complete Monitoring <br/> & Observability Platform</h1>
|
||||
<p class="mt-6 text-xl sm:text-2xl leading-8 text-gray-600">Monitor, Observe, Debug, Resolve. Everything you
|
||||
need to build reliable software in one open source platform.</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
@@ -117,15 +117,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile redirect helper - if JS is available and we detect mobile, redirect to dashboard
|
||||
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile Safari|Mobile/i.test(navigator.userAgent)) {
|
||||
// Small delay to avoid redirect loops and let server handle first
|
||||
setTimeout(() => {
|
||||
if (window.location.pathname === '/' && !window.location.search.includes('no-mobile-redirect')) {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
"ignore": [
|
||||
"greenlock.d/*"
|
||||
],
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -37,7 +37,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
logger.info(`IncomingRequestIngest Service - Queue concurrency: ${INCOMING_REQUEST_INGEST_CONCURRENCY}`);
|
||||
logger.info(
|
||||
`IncomingRequestIngest Service - Queue concurrency: ${INCOMING_REQUEST_INGEST_CONCURRENCY}`,
|
||||
);
|
||||
|
||||
// init the app
|
||||
await App.init({
|
||||
|
||||
@@ -6,6 +6,7 @@ import HTTPMethod from "Common/Types/API/HTTPMethod";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
||||
import MonitorType from "Common/Types/Monitor/MonitorType";
|
||||
@@ -31,9 +32,20 @@ QueueWorker.getWorker(
|
||||
`Successfully processed incoming request ingestion job: ${job.name}`,
|
||||
);
|
||||
} catch (error) {
|
||||
// Certain BadDataException cases are expected / non-actionable and should not fail the job.
|
||||
// These include disabled monitors (manual, maintenance, explicitly disabled) and missing monitors
|
||||
// (e.g. secret key referencing a deleted monitor). Retrying provides no value and only creates noise.
|
||||
if (
|
||||
error instanceof BadDataException &&
|
||||
(error.message === ExceptionMessages.MonitorNotFound ||
|
||||
error.message === ExceptionMessages.MonitorDisabled)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(`Error processing incoming request ingestion job:`);
|
||||
logger.error(error);
|
||||
throw error;
|
||||
throw error; // rethrow other errors so they are visible and retried if needed.
|
||||
}
|
||||
},
|
||||
{ concurrency: INCOMING_REQUEST_INGEST_CONCURRENCY }, // Configurable via env, defaults to 100
|
||||
@@ -78,7 +90,7 @@ async function processIncomingRequestFromQueue(
|
||||
});
|
||||
|
||||
if (!monitor || !monitor._id) {
|
||||
throw new BadDataException("Monitor not found");
|
||||
throw new BadDataException(ExceptionMessages.MonitorNotFound);
|
||||
}
|
||||
|
||||
if (!monitor.projectId) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"watch": ["./","../Common"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -7,3 +7,9 @@ if (typeof concurrency === "string") {
|
||||
}
|
||||
|
||||
export const OPEN_TELEMETRY_INGEST_CONCURRENCY: number = concurrency as number;
|
||||
|
||||
// Some telemetry batches can be large and take >30s (BullMQ default lock) to process.
|
||||
// Allow configuring a longer lock duration (in ms) to avoid premature stall detection.
|
||||
|
||||
// 10 minutes.
|
||||
export const OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS: number = 10 * 60 * 1000;
|
||||
|
||||
@@ -37,7 +37,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
logger.info(`OpenTelemetryIngest Service - Queue concurrency: ${OPEN_TELEMETRY_INGEST_CONCURRENCY}`);
|
||||
logger.info(
|
||||
`OpenTelemetryIngest Service - Queue concurrency: ${OPEN_TELEMETRY_INGEST_CONCURRENCY}`,
|
||||
);
|
||||
|
||||
// init the app
|
||||
await App.init({
|
||||
|
||||
@@ -8,7 +8,10 @@ import logger from "Common/Server/Utils/Logger";
|
||||
import { QueueJob, QueueName } from "Common/Server/Infrastructure/Queue";
|
||||
import QueueWorker from "Common/Server/Infrastructure/QueueWorker";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import { OPEN_TELEMETRY_INGEST_CONCURRENCY } from "../../Config";
|
||||
import {
|
||||
OPEN_TELEMETRY_INGEST_CONCURRENCY,
|
||||
OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS,
|
||||
} from "../../Config";
|
||||
|
||||
// Set up the unified worker for processing telemetry queue
|
||||
QueueWorker.getWorker(
|
||||
@@ -59,7 +62,12 @@ QueueWorker.getWorker(
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{ concurrency: OPEN_TELEMETRY_INGEST_CONCURRENCY },
|
||||
{
|
||||
concurrency: OPEN_TELEMETRY_INGEST_CONCURRENCY,
|
||||
lockDuration: OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS,
|
||||
// allow a couple of stall recoveries before marking failed if genuinely stuck
|
||||
maxStalledCount: 2,
|
||||
},
|
||||
);
|
||||
|
||||
logger.debug("Unified telemetry worker initialized");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
|
||||
"ext": "ts,json,tsx,env,js,jsx,hbs",
|
||||
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
|
||||
"watchOptions": {"useFsEvents": false, "interval": 500},
|
||||
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
|
||||
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
|
||||
}
|
||||
@@ -84,3 +84,16 @@ export const PROBE_MONITOR_RETRY_LIMIT: number = process.env[
|
||||
export const PORT: Port = new Port(
|
||||
process.env["PORT"] ? parseInt(process.env["PORT"]) : 3874,
|
||||
);
|
||||
|
||||
// Proxy configuration for all HTTP/HTTPS requests made by the probe
|
||||
// HTTP_PROXY_URL: Proxy for HTTP requests
|
||||
// Format: http://[username:password@]proxy.example.com:port
|
||||
// Example: http://proxy.example.com:8080
|
||||
// Example with auth: http://user:pass@proxy.example.com:8080
|
||||
export const HTTP_PROXY_URL: string | null = process.env["HTTP_PROXY_URL"] || process.env["http_proxy"] || null;
|
||||
|
||||
// HTTPS_PROXY_URL: Proxy for HTTPS requests
|
||||
// Format: http://[username:password@]proxy.example.com:port
|
||||
// Example: http://proxy.example.com:8080
|
||||
// Example with auth: http://user:pass@proxy.example.com:8080
|
||||
export const HTTPS_PROXY_URL: string | null = process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"] || null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user