Compare commits
111 Commits
dash-chunk
...
safe-vm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d62816dd49 | ||
|
|
7dd6129dad | ||
|
|
7ccea02340 | ||
|
|
0af41725b4 | ||
|
|
9f6bcddc1e | ||
|
|
97c461f7a3 | ||
|
|
736f8bb83c | ||
|
|
eb33daf64f | ||
|
|
c3c90eef03 | ||
|
|
e92e9f08d3 | ||
|
|
2b313a7702 | ||
|
|
3cf7c7d1ae | ||
|
|
76cfa7186e | ||
|
|
afaff717c0 | ||
|
|
fde0d5f2c6 | ||
|
|
d5c5387621 | ||
|
|
e0ef6e9a77 | ||
|
|
2dc0dc4c96 | ||
|
|
c9eb72ba2c | ||
|
|
92e247d168 | ||
|
|
14988c438a | ||
|
|
d81682d02f | ||
|
|
9d5faca3ec | ||
|
|
89ccde1bc4 | ||
|
|
3aab280dcd | ||
|
|
b8e44a1bcf | ||
|
|
4c3b4d23ff | ||
|
|
a4ff718d61 | ||
|
|
3433a815f3 | ||
|
|
2a20807126 | ||
|
|
991dc1c842 | ||
|
|
2026e7fd77 | ||
|
|
1d0016412e | ||
|
|
917f27fe11 | ||
|
|
c07c89e3dd | ||
|
|
32c4c1666d | ||
|
|
636a419cbd | ||
|
|
61699b9f4a | ||
|
|
b6ed3643c3 | ||
|
|
9e73ac45a1 | ||
|
|
7a3dbd0e8e | ||
|
|
1ec25c27ee | ||
|
|
5286527155 | ||
|
|
895af10755 | ||
|
|
77ccca7e2a | ||
|
|
66f46e9b84 | ||
|
|
91edae50b2 | ||
|
|
7ab3dfe043 | ||
|
|
fb661126d4 | ||
|
|
94c57f3189 | ||
|
|
4de6021905 | ||
|
|
c62a49d499 | ||
|
|
01fd5263ca | ||
|
|
d87eee68e8 | ||
|
|
3f4db5b7e0 | ||
|
|
462ad9d6ab | ||
|
|
6444d3d5cc | ||
|
|
415222561b | ||
|
|
8cf2661c63 | ||
|
|
a820f817ff | ||
|
|
576927c6c7 | ||
|
|
e866db9e18 | ||
|
|
8e91a786f9 | ||
|
|
02d16446f1 | ||
|
|
5d5517258b | ||
|
|
5df632c46c | ||
|
|
c1ee79b339 | ||
|
|
67265c0fc8 | ||
|
|
72e5384012 | ||
|
|
dc8e9d44b1 | ||
|
|
91102ee952 | ||
|
|
e46d1ae7da | ||
|
|
008005415a | ||
|
|
c7362f3ada | ||
|
|
1f634576fe | ||
|
|
d25a97fe17 | ||
|
|
b89ff11db8 | ||
|
|
5ac5ffede5 | ||
|
|
d9167b89ba | ||
|
|
66b995c64a | ||
|
|
f383bbba4d | ||
|
|
43f1a59042 | ||
|
|
7d49872edc | ||
|
|
6d2d5892b9 | ||
|
|
756217e19e | ||
|
|
ca3cf01be7 | ||
|
|
fd0a81a0b1 | ||
|
|
14016d188b | ||
|
|
3a2aff7f34 | ||
|
|
4a6edfee06 | ||
|
|
2dc1b8aa8c | ||
|
|
eb0f0e742d | ||
|
|
23c82c5239 | ||
|
|
2b61e4f4b7 | ||
|
|
9b21abf78d | ||
|
|
bd54b38a69 | ||
|
|
4dc799d238 | ||
|
|
b4d90e3bef | ||
|
|
6c8d4203da | ||
|
|
f7e9745624 | ||
|
|
f7d133adba | ||
|
|
b06c2cb1c3 | ||
|
|
b51c5d9677 | ||
|
|
9a1e265d1c | ||
|
|
e18d75fc8e | ||
|
|
5a68d2f726 | ||
|
|
dfa7c4875a | ||
|
|
8a568e0495 | ||
|
|
7152058ee2 | ||
|
|
9dfbc05618 | ||
|
|
e32d4395a3 |
20
.github/workflows/compile.yml
vendored
@@ -398,6 +398,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install && npm run compile
|
||||
- name: Compile MobileApp
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
@@ -420,4 +421,21 @@ jobs:
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd AIAgent && npm install && npm run compile && npm run dep-check
|
||||
command: cd AIAgent && npm install && npm run compile && npm run dep-check
|
||||
|
||||
compile-cli:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- name: Compile CLI
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
command: cd CLI && npm install && npm run compile && npm run dep-check
|
||||
21
.github/workflows/test.cli.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: CLI Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'hotfix-*' # excludes hotfix branches
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_PIPELINE_ID: ${{github.run_number}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
- run: cd Common && npm install
|
||||
- run: cd CLI && npm install && npm run test
|
||||
26
Accounts/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
26
AdminDashboard/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
@@ -73,6 +73,46 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardModelDetail
|
||||
name="Project Creation Settings"
|
||||
cardProps={{
|
||||
title: "Project Creation",
|
||||
description:
|
||||
"Control who can create new projects on this OneUptime Server.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Settings"
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
disableUserProjectCreation: true,
|
||||
},
|
||||
title: "Restrict Project Creation to Admins Only",
|
||||
fieldType: FormFieldSchemaType.Toggle,
|
||||
required: false,
|
||||
description:
|
||||
"When enabled, only master admin users can create new projects.",
|
||||
},
|
||||
]}
|
||||
modelDetailProps={{
|
||||
modelType: GlobalConfig,
|
||||
id: "model-detail-project-creation",
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
disableUserProjectCreation: true,
|
||||
},
|
||||
fieldType: FieldType.Boolean,
|
||||
title: "Restrict Project Creation to Admins Only",
|
||||
placeholder: "No",
|
||||
description:
|
||||
"When enabled, only master admin users can create new projects.",
|
||||
},
|
||||
],
|
||||
modelId: ObjectID.getZeroObjectID(),
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
105
App/FeatureSet/Notification/API/PushRelay.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import Response from "Common/Server/Utils/Response";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import PushNotificationService from "Common/Server/Services/PushNotificationService";
|
||||
|
||||
const router: ExpressRouter = Express.getRouter();
|
||||
|
||||
// Simple in-memory rate limiter by IP
|
||||
const rateLimitMap: Map<string, { count: number; resetTime: number }> =
|
||||
new Map();
|
||||
const RATE_LIMIT_WINDOW_MS: number = 60 * 1000; // 1 minute
|
||||
const RATE_LIMIT_MAX_REQUESTS: number = 60; // 60 requests per minute per IP
|
||||
|
||||
function isRateLimited(ip: string): boolean {
|
||||
const now: number = Date.now();
|
||||
const entry: { count: number; resetTime: number } | undefined =
|
||||
rateLimitMap.get(ip);
|
||||
|
||||
if (!entry || now > entry.resetTime) {
|
||||
rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
|
||||
return entry.count > RATE_LIMIT_MAX_REQUESTS;
|
||||
}
|
||||
|
||||
// Clean up stale rate limit entries every 5 minutes
|
||||
setInterval(() => {
|
||||
const now: number = Date.now();
|
||||
for (const [ip, entry] of rateLimitMap.entries()) {
|
||||
if (now > entry.resetTime) {
|
||||
rateLimitMap.delete(ip);
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
router.post(
|
||||
"/send",
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
const clientIp: string =
|
||||
(req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() ||
|
||||
req.socket.remoteAddress ||
|
||||
"unknown";
|
||||
|
||||
if (isRateLimited(clientIp)) {
|
||||
res.status(429).json({
|
||||
message: "Rate limit exceeded. Please try again later.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PushNotificationService.hasExpoAccessToken()) {
|
||||
throw new BadDataException(
|
||||
"Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
|
||||
);
|
||||
}
|
||||
|
||||
const body: JSONObject = req.body as JSONObject;
|
||||
|
||||
const to: string | undefined = body["to"] as string | undefined;
|
||||
|
||||
if (!to || !PushNotificationService.isValidExpoPushToken(to)) {
|
||||
throw new BadDataException(
|
||||
"Invalid or missing push token. Must be a valid Expo push token.",
|
||||
);
|
||||
}
|
||||
|
||||
const title: string | undefined = body["title"] as string | undefined;
|
||||
const messageBody: string | undefined = body["body"] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!title && !messageBody) {
|
||||
throw new BadDataException(
|
||||
"At least one of 'title' or 'body' must be provided.",
|
||||
);
|
||||
}
|
||||
|
||||
await PushNotificationService.sendRelayPushNotification({
|
||||
to: to,
|
||||
title: title,
|
||||
body: messageBody,
|
||||
data: (body["data"] as { [key: string]: string }) || {},
|
||||
sound: (body["sound"] as string) || "default",
|
||||
priority: (body["priority"] as string) || "high",
|
||||
channelId: (body["channelId"] as string) || "default",
|
||||
});
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, { success: true });
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -4,6 +4,7 @@ import MailAPI from "./API/Mail";
|
||||
import SmsAPI from "./API/SMS";
|
||||
import WhatsAppAPI from "./API/WhatsApp";
|
||||
import PushNotificationAPI from "./API/PushNotification";
|
||||
import PushRelayAPI from "./API/PushRelay";
|
||||
import SMTPConfigAPI from "./API/SMTPConfig";
|
||||
import PhoneNumberAPI from "./API/PhoneNumber";
|
||||
import IncomingCallAPI from "./API/IncomingCall";
|
||||
@@ -21,6 +22,7 @@ const NotificationFeatureSet: FeatureSet = {
|
||||
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
|
||||
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
|
||||
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
|
||||
app.use([`/${APP_NAME}/push-relay`, "/push-relay"], PushRelayAPI);
|
||||
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
|
||||
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
|
||||
app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI);
|
||||
|
||||
58
App/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@sendgrid/mail": "^8.1.0",
|
||||
"Common": "file:../Common",
|
||||
"ejs": "^3.1.9",
|
||||
"expo-server-sdk": "^5.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"nodemailer": "^6.9.7",
|
||||
"ts-node": "^10.9.1",
|
||||
@@ -2166,6 +2167,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/err-code": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -2299,6 +2306,20 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-server-sdk": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-5.0.0.tgz",
|
||||
"integrity": "sha512-GEp1XYLU80iS/hdRo3c2n092E8TgTXcHSuw6Lw68dSoWaAgiLPI2R+e5hp5+hGF1TtJZOi2nxtJX63+XA3iz9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-limit": "^2.7.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"undici": "^7.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -4167,6 +4188,25 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-limit": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
|
||||
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/promise-retry": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
|
||||
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"err-code": "^2.0.2",
|
||||
"retry": "^0.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -4290,6 +4330,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -4801,6 +4850,15 @@
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
|
||||
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@sendgrid/mail": "^8.1.0",
|
||||
"Common": "file:../Common",
|
||||
"ejs": "^3.1.9",
|
||||
"expo-server-sdk": "^5.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"nodemailer": "^6.9.7",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
140
CLI/Commands/ConfigCommands.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Command } from "commander";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import { CLIContext } from "../Types/CLITypes";
|
||||
import { printSuccess, printError, printInfo } from "../Core/OutputFormatter";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function registerConfigCommands(program: Command): void {
|
||||
// Login command
|
||||
const loginCmd: Command = program
|
||||
.command("login")
|
||||
.description("Authenticate with a OneUptime instance")
|
||||
.argument("<api-key>", "API key for authentication")
|
||||
.argument(
|
||||
"<instance-url>",
|
||||
"OneUptime instance URL (e.g. https://oneuptime.com)",
|
||||
)
|
||||
.option("--context-name <name>", "Name for this context", "default")
|
||||
.action(
|
||||
(
|
||||
apiKey: string,
|
||||
instanceUrl: string,
|
||||
options: { contextName: string },
|
||||
) => {
|
||||
try {
|
||||
const context: CLIContext = {
|
||||
name: options.contextName,
|
||||
apiUrl: instanceUrl.replace(/\/+$/, ""),
|
||||
apiKey: apiKey,
|
||||
};
|
||||
|
||||
ConfigManager.addContext(context);
|
||||
ConfigManager.setCurrentContext(context.name);
|
||||
|
||||
printSuccess(
|
||||
`Logged in successfully. Context "${context.name}" is now active.`,
|
||||
);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Login failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Suppress unused variable warning - loginCmd is used for registration
|
||||
void loginCmd;
|
||||
|
||||
// Context commands
|
||||
const contextCmd: Command = program
|
||||
.command("context")
|
||||
.description("Manage CLI contexts (environments/projects)");
|
||||
|
||||
contextCmd
|
||||
.command("list")
|
||||
.description("List all configured contexts")
|
||||
.action(() => {
|
||||
const contexts: Array<CLIContext & { isCurrent: boolean }> =
|
||||
ConfigManager.listContexts();
|
||||
|
||||
if (contexts.length === 0) {
|
||||
printInfo(
|
||||
"No contexts configured. Run `oneuptime login` to create one.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const noColor: boolean =
|
||||
process.env["NO_COLOR"] !== undefined ||
|
||||
process.argv.includes("--no-color");
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["", "Name", "URL"].map((h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
}),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const ctx of contexts) {
|
||||
table.push([ctx.isCurrent ? "*" : "", ctx.name, ctx.apiUrl]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("use <name>")
|
||||
.description("Switch to a different context")
|
||||
.action((name: string) => {
|
||||
try {
|
||||
ConfigManager.setCurrentContext(name);
|
||||
printSuccess(`Switched to context "${name}".`);
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("current")
|
||||
.description("Show the current active context")
|
||||
.action(() => {
|
||||
const ctx: CLIContext | null = ConfigManager.getCurrentContext();
|
||||
if (!ctx) {
|
||||
printInfo(
|
||||
"No current context set. Run `oneuptime login` to create one.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maskedKey: string =
|
||||
ctx.apiKey.length > 8
|
||||
? ctx.apiKey.substring(0, 4) +
|
||||
"****" +
|
||||
ctx.apiKey.substring(ctx.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${ctx.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
});
|
||||
|
||||
contextCmd
|
||||
.command("delete <name>")
|
||||
.description("Delete a context")
|
||||
.action((name: string) => {
|
||||
try {
|
||||
ConfigManager.removeContext(name);
|
||||
printSuccess(`Context "${name}" deleted.`);
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
356
CLI/Commands/ResourceCommands.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { Command } from "commander";
|
||||
import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import { ResourceInfo, ResolvedCredentials } from "../Types/CLITypes";
|
||||
import { executeApiRequest, ApiOperation } from "../Core/ApiClient";
|
||||
import { CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
|
||||
import { formatOutput, printSuccess } from "../Core/OutputFormatter";
|
||||
import { handleError } from "../Core/ErrorHandler";
|
||||
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
|
||||
import { JSONObject, JSONValue } from "Common/Types/JSON";
|
||||
import * as fs from "fs";
|
||||
|
||||
function toKebabCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
||||
.replace(/[\s_]+/g, "-")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function parseJsonArg(value: string): JSONObject {
|
||||
try {
|
||||
return JSON.parse(value) as JSONObject;
|
||||
} catch {
|
||||
throw new Error(`Invalid JSON: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function discoverResources(): ResourceInfo[] {
|
||||
const resources: ResourceInfo[] = [];
|
||||
|
||||
// Database models
|
||||
for (const ModelClass of DatabaseModels) {
|
||||
try {
|
||||
const model: BaseModel = new ModelClass();
|
||||
const tableName: string = model.tableName || ModelClass.name;
|
||||
const singularName: string = model.singularName || tableName;
|
||||
const pluralName: string = model.pluralName || `${singularName}s`;
|
||||
const apiPath: string | undefined = model.crudApiPath?.toString();
|
||||
|
||||
if (tableName && model.enableMCP && apiPath) {
|
||||
resources.push({
|
||||
name: toKebabCase(singularName),
|
||||
singularName,
|
||||
pluralName,
|
||||
apiPath,
|
||||
tableName,
|
||||
modelType: "database",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip models that fail to instantiate
|
||||
}
|
||||
}
|
||||
|
||||
// Analytics models
|
||||
for (const ModelClass of AnalyticsModels) {
|
||||
try {
|
||||
const model: AnalyticsBaseModel = new ModelClass();
|
||||
const tableName: string = model.tableName || ModelClass.name;
|
||||
const singularName: string = model.singularName || tableName;
|
||||
const pluralName: string = model.pluralName || `${singularName}s`;
|
||||
const apiPath: string | undefined = model.crudApiPath?.toString();
|
||||
|
||||
if (tableName && model.enableMCP && apiPath) {
|
||||
resources.push({
|
||||
name: toKebabCase(singularName),
|
||||
singularName,
|
||||
pluralName,
|
||||
apiPath,
|
||||
tableName,
|
||||
modelType: "analytics",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip models that fail to instantiate
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
function getParentOptions(cmd: Command): CLIOptions {
|
||||
// Walk up to root program to get global options
|
||||
let current: Command | null = cmd;
|
||||
while (current?.parent) {
|
||||
current = current.parent;
|
||||
}
|
||||
const opts: Record<string, unknown> = current?.opts() || {};
|
||||
return {
|
||||
apiKey: opts["apiKey"] as string | undefined,
|
||||
url: opts["url"] as string | undefined,
|
||||
context: opts["context"] as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function registerListCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("list")
|
||||
.description(`List ${resource.pluralName}`)
|
||||
.option("--query <json>", "Filter query as JSON")
|
||||
.option("--limit <n>", "Max results to return", "10")
|
||||
.option("--skip <n>", "Number of results to skip", "0")
|
||||
.option("--sort <json>", "Sort order as JSON")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (options: {
|
||||
query?: string;
|
||||
limit: string;
|
||||
skip: string;
|
||||
sort?: string;
|
||||
output?: string;
|
||||
}) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "list" as ApiOperation,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
select,
|
||||
skip: parseInt(options.skip, 10),
|
||||
limit: parseInt(options.limit, 10),
|
||||
sort: options.sort ? parseJsonArg(options.sort) : undefined,
|
||||
});
|
||||
|
||||
// Extract data array from response
|
||||
const responseData: JSONValue =
|
||||
result && typeof result === "object" && !Array.isArray(result)
|
||||
? ((result as JSONObject)["data"] as JSONValue) || result
|
||||
: result;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(responseData, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function registerGetCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("get <id>")
|
||||
.description(`Get a single ${resource.singularName} by ID`)
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(async (id: string, options: { output?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "read" as ApiOperation,
|
||||
id,
|
||||
select,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerCreateCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("create")
|
||||
.description(`Create a new ${resource.singularName}`)
|
||||
.option("--data <json>", "Resource data as JSON")
|
||||
.option("--file <path>", "Read resource data from a JSON file")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (options: { data?: string; file?: string; output?: string }) => {
|
||||
try {
|
||||
let data: JSONObject;
|
||||
|
||||
if (options.file) {
|
||||
const fileContent: string = fs.readFileSync(options.file, "utf-8");
|
||||
data = JSON.parse(fileContent) as JSONObject;
|
||||
} else if (options.data) {
|
||||
data = parseJsonArg(options.data);
|
||||
} else {
|
||||
throw new Error("Either --data or --file is required for create.");
|
||||
}
|
||||
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "create" as ApiOperation,
|
||||
data,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function registerUpdateCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("update <id>")
|
||||
.description(`Update an existing ${resource.singularName}`)
|
||||
.requiredOption("--data <json>", "Fields to update as JSON")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(async (id: string, options: { data: string; output?: string }) => {
|
||||
try {
|
||||
const data: JSONObject = parseJsonArg(options.data);
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "update" as ApiOperation,
|
||||
id,
|
||||
data,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerDeleteCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("delete <id>")
|
||||
.description(`Delete a ${resource.singularName}`)
|
||||
.option("--force", "Skip confirmation")
|
||||
.action(async (id: string, _options: { force?: boolean }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "delete" as ApiOperation,
|
||||
id,
|
||||
});
|
||||
|
||||
printSuccess(`${resource.singularName} ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerCountCommand(
|
||||
resourceCmd: Command,
|
||||
resource: ResourceInfo,
|
||||
): void {
|
||||
resourceCmd
|
||||
.command("count")
|
||||
.description(`Count ${resource.pluralName}`)
|
||||
.option("--query <json>", "Filter query as JSON")
|
||||
.action(async (options: { query?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "count" as ApiOperation,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
});
|
||||
|
||||
// Count response is typically { count: number }
|
||||
if (
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
!Array.isArray(result) &&
|
||||
"count" in (result as JSONObject)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log((result as JSONObject)["count"]);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(result);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function registerResourceCommands(program: Command): void {
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
|
||||
for (const resource of resources) {
|
||||
const resourceCmd: Command = program
|
||||
.command(resource.name)
|
||||
.description(`Manage ${resource.pluralName} (${resource.modelType})`);
|
||||
|
||||
// Database models get full CRUD
|
||||
if (resource.modelType === "database") {
|
||||
registerListCommand(resourceCmd, resource);
|
||||
registerGetCommand(resourceCmd, resource);
|
||||
registerCreateCommand(resourceCmd, resource);
|
||||
registerUpdateCommand(resourceCmd, resource);
|
||||
registerDeleteCommand(resourceCmd, resource);
|
||||
registerCountCommand(resourceCmd, resource);
|
||||
}
|
||||
|
||||
// Analytics models get create, list, count
|
||||
if (resource.modelType === "analytics") {
|
||||
registerListCommand(resourceCmd, resource);
|
||||
registerCreateCommand(resourceCmd, resource);
|
||||
registerCountCommand(resourceCmd, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
CLI/Commands/UtilityCommands.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
CLIContext,
|
||||
ResolvedCredentials,
|
||||
ResourceInfo,
|
||||
} from "../Types/CLITypes";
|
||||
import {
|
||||
getCurrentContext,
|
||||
CLIOptions,
|
||||
getResolvedCredentials,
|
||||
} from "../Core/ConfigManager";
|
||||
import { printInfo, printError } from "../Core/OutputFormatter";
|
||||
import { discoverResources } from "./ResourceCommands";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function registerUtilityCommands(program: Command): void {
|
||||
// Version command
|
||||
program
|
||||
.command("version")
|
||||
.description("Print CLI version")
|
||||
.action(() => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const pkg: { version: string } = require("../package.json") as {
|
||||
version: string;
|
||||
};
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(pkg.version);
|
||||
} catch {
|
||||
// Fallback if package.json can't be loaded at runtime
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("1.0.0");
|
||||
}
|
||||
});
|
||||
|
||||
// Whoami command
|
||||
program
|
||||
.command("whoami")
|
||||
.description("Show current authentication info")
|
||||
.action(() => {
|
||||
try {
|
||||
const ctx: CLIContext | null = getCurrentContext();
|
||||
const opts: Record<string, unknown> = program.opts();
|
||||
const cliOpts: CLIOptions = {
|
||||
apiKey: opts["apiKey"] as string | undefined,
|
||||
url: opts["url"] as string | undefined,
|
||||
context: opts["context"] as string | undefined,
|
||||
};
|
||||
|
||||
let creds: ResolvedCredentials;
|
||||
try {
|
||||
creds = getResolvedCredentials(cliOpts);
|
||||
} catch {
|
||||
printInfo(
|
||||
"Not authenticated. Run `oneuptime login` to authenticate.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const maskedKey: string =
|
||||
creds.apiKey.length > 8
|
||||
? creds.apiKey.substring(0, 4) +
|
||||
"****" +
|
||||
creds.apiKey.substring(creds.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${creds.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
if (ctx) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Resources command
|
||||
program
|
||||
.command("resources")
|
||||
.description("List all available resource types")
|
||||
.option("--type <type>", "Filter by model type: database, analytics")
|
||||
.action((options: { type?: string }) => {
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
|
||||
const filtered: ResourceInfo[] = options.type
|
||||
? resources.filter((r: ResourceInfo) => {
|
||||
return r.modelType === options.type;
|
||||
})
|
||||
: resources;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
printInfo("No resources found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const noColor: boolean =
|
||||
process.env["NO_COLOR"] !== undefined ||
|
||||
process.argv.includes("--no-color");
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["Command", "Singular", "Plural", "Type", "API Path"].map(
|
||||
(h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
},
|
||||
),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const r of filtered) {
|
||||
table.push([
|
||||
r.name,
|
||||
r.singularName,
|
||||
r.pluralName,
|
||||
r.modelType,
|
||||
r.apiPath,
|
||||
]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\nTotal: ${filtered.length} resources`);
|
||||
});
|
||||
}
|
||||
150
CLI/Core/ApiClient.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import API from "Common/Utils/API";
|
||||
import URL from "Common/Types/API/URL";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import Headers from "Common/Types/API/Headers";
|
||||
import HTTPResponse from "Common/Types/API/HTTPResponse";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import Protocol from "Common/Types/API/Protocol";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
import { JSONObject, JSONValue } from "Common/Types/JSON";
|
||||
|
||||
export type ApiOperation =
|
||||
| "create"
|
||||
| "read"
|
||||
| "list"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "count";
|
||||
|
||||
export interface ApiRequestOptions {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
apiPath: string;
|
||||
operation: ApiOperation;
|
||||
id?: string | undefined;
|
||||
data?: JSONObject | undefined;
|
||||
query?: JSONObject | undefined;
|
||||
select?: JSONObject | undefined;
|
||||
skip?: number | undefined;
|
||||
limit?: number | undefined;
|
||||
sort?: JSONObject | undefined;
|
||||
}
|
||||
|
||||
function buildApiRoute(
|
||||
apiPath: string,
|
||||
operation: ApiOperation,
|
||||
id?: string,
|
||||
): Route {
|
||||
let fullPath: string = `/api${apiPath}`;
|
||||
|
||||
switch (operation) {
|
||||
case "read":
|
||||
if (id) {
|
||||
fullPath = `/api${apiPath}/${id}/get-item`;
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
case "delete":
|
||||
if (id) {
|
||||
fullPath = `/api${apiPath}/${id}/`;
|
||||
}
|
||||
break;
|
||||
case "count":
|
||||
fullPath = `/api${apiPath}/count`;
|
||||
break;
|
||||
case "list":
|
||||
fullPath = `/api${apiPath}/get-list`;
|
||||
break;
|
||||
case "create":
|
||||
default:
|
||||
fullPath = `/api${apiPath}`;
|
||||
break;
|
||||
}
|
||||
|
||||
return new Route(fullPath);
|
||||
}
|
||||
|
||||
function buildHeaders(apiKey: string): Headers {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
APIKey: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestData(options: ApiRequestOptions): JSONObject | undefined {
|
||||
switch (options.operation) {
|
||||
case "create":
|
||||
return { data: options.data || {} } as JSONObject;
|
||||
case "update":
|
||||
return { data: options.data || {} } as JSONObject;
|
||||
case "list":
|
||||
case "count":
|
||||
return {
|
||||
query: options.query || {},
|
||||
select: options.select || {},
|
||||
skip: options.skip || 0,
|
||||
limit: options.limit || 10,
|
||||
sort: options.sort || {},
|
||||
} as JSONObject;
|
||||
case "read":
|
||||
return {
|
||||
select: options.select || {},
|
||||
} as JSONObject;
|
||||
case "delete":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeApiRequest(
|
||||
options: ApiRequestOptions,
|
||||
): Promise<JSONValue> {
|
||||
const url: URL = URL.fromString(options.apiUrl);
|
||||
const protocol: Protocol = url.protocol;
|
||||
const hostname: Hostname = url.hostname;
|
||||
|
||||
const api: API = new API(protocol, hostname, new Route("/"));
|
||||
const route: Route = buildApiRoute(
|
||||
options.apiPath,
|
||||
options.operation,
|
||||
options.id,
|
||||
);
|
||||
const headers: Headers = buildHeaders(options.apiKey);
|
||||
const data: JSONObject | undefined = buildRequestData(options);
|
||||
|
||||
const requestUrl: URL = new URL(api.protocol, api.hostname, route);
|
||||
const baseOptions: { url: URL; headers: Headers } = {
|
||||
url: requestUrl,
|
||||
headers,
|
||||
};
|
||||
|
||||
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
|
||||
|
||||
switch (options.operation) {
|
||||
case "create":
|
||||
case "count":
|
||||
case "list":
|
||||
case "read":
|
||||
response = await API.post(data ? { ...baseOptions, data } : baseOptions);
|
||||
break;
|
||||
case "update":
|
||||
response = await API.put(data ? { ...baseOptions, data } : baseOptions);
|
||||
break;
|
||||
case "delete":
|
||||
response = await API.delete(
|
||||
data ? { ...baseOptions, data } : baseOptions,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operation: ${options.operation}`);
|
||||
}
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw new Error(
|
||||
`API error (${response.statusCode}): ${response.message || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
141
CLI/Core/ConfigManager.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { CLIConfig, CLIContext, ResolvedCredentials } from "../Types/CLITypes";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
function getDefaultConfig(): CLIConfig {
|
||||
return {
|
||||
currentContext: "",
|
||||
contexts: {},
|
||||
defaults: {
|
||||
output: "table",
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function load(): CLIConfig {
|
||||
try {
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
const raw: string = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
return JSON.parse(raw) as CLIConfig;
|
||||
} catch {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
export function save(config: CLIConfig): void {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
||||
mode: 0o600,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCurrentContext(): CLIContext | null {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.currentContext) {
|
||||
return null;
|
||||
}
|
||||
return config.contexts[config.currentContext] || null;
|
||||
}
|
||||
|
||||
export function setCurrentContext(name: string): void {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.contexts[name]) {
|
||||
throw new Error(`Context "${name}" does not exist.`);
|
||||
}
|
||||
config.currentContext = name;
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function addContext(context: CLIContext): void {
|
||||
const config: CLIConfig = load();
|
||||
config.contexts[context.name] = context;
|
||||
if (!config.currentContext) {
|
||||
config.currentContext = context.name;
|
||||
}
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function removeContext(name: string): void {
|
||||
const config: CLIConfig = load();
|
||||
if (!config.contexts[name]) {
|
||||
throw new Error(`Context "${name}" does not exist.`);
|
||||
}
|
||||
delete config.contexts[name];
|
||||
if (config.currentContext === name) {
|
||||
const remaining: string[] = Object.keys(config.contexts);
|
||||
config.currentContext = remaining[0] || "";
|
||||
}
|
||||
save(config);
|
||||
}
|
||||
|
||||
export function listContexts(): Array<CLIContext & { isCurrent: boolean }> {
|
||||
const config: CLIConfig = load();
|
||||
return Object.values(config.contexts).map(
|
||||
(ctx: CLIContext): CLIContext & { isCurrent: boolean } => {
|
||||
return {
|
||||
...ctx,
|
||||
isCurrent: ctx.name === config.currentContext,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export interface CLIOptions {
|
||||
apiKey?: string | undefined;
|
||||
url?: string | undefined;
|
||||
context?: string | undefined;
|
||||
}
|
||||
|
||||
export function getResolvedCredentials(
|
||||
cliOptions: CLIOptions,
|
||||
): ResolvedCredentials {
|
||||
// Priority 1: CLI flags
|
||||
if (cliOptions.apiKey && cliOptions.url) {
|
||||
return { apiKey: cliOptions.apiKey, apiUrl: cliOptions.url };
|
||||
}
|
||||
|
||||
// Priority 2: Environment variables
|
||||
const envApiKey: string | undefined = process.env["ONEUPTIME_API_KEY"];
|
||||
const envUrl: string | undefined = process.env["ONEUPTIME_URL"];
|
||||
if (envApiKey && envUrl) {
|
||||
return { apiKey: envApiKey, apiUrl: envUrl };
|
||||
}
|
||||
|
||||
// Priority 3: Specific context if specified via --context flag
|
||||
if (cliOptions.context) {
|
||||
const config: CLIConfig = load();
|
||||
const ctx: CLIContext | undefined = config.contexts[cliOptions.context];
|
||||
if (ctx) {
|
||||
return { apiKey: ctx.apiKey, apiUrl: ctx.apiUrl };
|
||||
}
|
||||
throw new Error(`Context "${cliOptions.context}" does not exist.`);
|
||||
}
|
||||
|
||||
// Priority 4: Current context in config file
|
||||
const currentCtx: CLIContext | null = getCurrentContext();
|
||||
if (currentCtx) {
|
||||
return { apiKey: currentCtx.apiKey, apiUrl: currentCtx.apiUrl };
|
||||
}
|
||||
|
||||
// Partial env vars + partial context
|
||||
if (envApiKey || envUrl) {
|
||||
const ctx: CLIContext | null = getCurrentContext();
|
||||
return {
|
||||
apiKey: envApiKey || ctx?.apiKey || "",
|
||||
apiUrl: envUrl || ctx?.apiUrl || "",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"No credentials found. Run `oneuptime login` or set ONEUPTIME_API_KEY and ONEUPTIME_URL environment variables.",
|
||||
);
|
||||
}
|
||||
43
CLI/Core/ErrorHandler.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { printError } from "./OutputFormatter";
|
||||
|
||||
export enum ExitCode {
|
||||
Success = 0,
|
||||
GeneralError = 1,
|
||||
AuthError = 2,
|
||||
NotFound = 3,
|
||||
}
|
||||
|
||||
export function handleError(error: unknown): never {
|
||||
if (error instanceof Error) {
|
||||
const message: string = error.message;
|
||||
|
||||
// Check for auth-related errors
|
||||
if (
|
||||
message.includes("API key") ||
|
||||
message.includes("credentials") ||
|
||||
message.includes("Unauthorized") ||
|
||||
message.includes("401")
|
||||
) {
|
||||
printError(`Authentication error: ${message}`);
|
||||
process.exit(ExitCode.AuthError);
|
||||
}
|
||||
|
||||
// Check for not found errors
|
||||
if (message.includes("404") || message.includes("not found")) {
|
||||
printError(`Not found: ${message}`);
|
||||
process.exit(ExitCode.NotFound);
|
||||
}
|
||||
|
||||
// General API errors
|
||||
if (message.includes("API error")) {
|
||||
printError(message);
|
||||
process.exit(ExitCode.GeneralError);
|
||||
}
|
||||
|
||||
printError(`Error: ${message}`);
|
||||
} else {
|
||||
printError(`Error: ${String(error)}`);
|
||||
}
|
||||
|
||||
process.exit(ExitCode.GeneralError);
|
||||
}
|
||||
195
CLI/Core/OutputFormatter.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { OutputFormat } from "../Types/CLITypes";
|
||||
import { JSONValue, JSONObject, JSONArray } from "Common/Types/JSON";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
function isColorDisabled(): boolean {
|
||||
return (
|
||||
process.env["NO_COLOR"] !== undefined || process.argv.includes("--no-color")
|
||||
);
|
||||
}
|
||||
|
||||
function detectOutputFormat(cliFormat?: string): OutputFormat {
|
||||
if (cliFormat) {
|
||||
if (cliFormat === "json") {
|
||||
return OutputFormat.JSON;
|
||||
}
|
||||
if (cliFormat === "wide") {
|
||||
return OutputFormat.Wide;
|
||||
}
|
||||
if (cliFormat === "table") {
|
||||
return OutputFormat.Table;
|
||||
}
|
||||
}
|
||||
|
||||
// If stdout is not a TTY (piped), default to JSON
|
||||
if (!process.stdout.isTTY) {
|
||||
return OutputFormat.JSON;
|
||||
}
|
||||
|
||||
return OutputFormat.Table;
|
||||
}
|
||||
|
||||
function formatJson(data: JSONValue): string {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
function formatTable(data: JSONValue, wide: boolean): string {
|
||||
if (!data) {
|
||||
return "No data returned.";
|
||||
}
|
||||
|
||||
// Handle single object
|
||||
if (!Array.isArray(data)) {
|
||||
return formatSingleObject(data as JSONObject);
|
||||
}
|
||||
|
||||
const items: JSONArray = data as JSONArray;
|
||||
if (items.length === 0) {
|
||||
return "No results found.";
|
||||
}
|
||||
|
||||
// Get all keys from the first item
|
||||
const firstItem: JSONObject = items[0] as JSONObject;
|
||||
if (!firstItem || typeof firstItem !== "object") {
|
||||
return formatJson(data);
|
||||
}
|
||||
|
||||
let columns: string[] = Object.keys(firstItem);
|
||||
|
||||
// In non-wide mode, limit columns to keep the table readable
|
||||
if (!wide && columns.length > 6) {
|
||||
// Prioritize common fields
|
||||
const priority: string[] = [
|
||||
"_id",
|
||||
"name",
|
||||
"title",
|
||||
"status",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
];
|
||||
const prioritized: string[] = priority.filter((col: string) => {
|
||||
return columns.includes(col);
|
||||
});
|
||||
const remaining: string[] = columns.filter((col: string) => {
|
||||
return !priority.includes(col);
|
||||
});
|
||||
columns = [...prioritized, ...remaining].slice(0, 6);
|
||||
}
|
||||
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: columns.map((col: string) => {
|
||||
return useColor ? chalk.cyan(col) : col;
|
||||
}),
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
},
|
||||
wordWrap: true,
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
const row: string[] = columns.map((col: string) => {
|
||||
const val: JSONValue = (item as JSONObject)[col] as JSONValue;
|
||||
return truncateValue(val);
|
||||
});
|
||||
table.push(row);
|
||||
}
|
||||
|
||||
return table.toString();
|
||||
}
|
||||
|
||||
function formatSingleObject(obj: JSONObject): string {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const label: string = useColor ? chalk.cyan(key) : key;
|
||||
table.push({ [label]: truncateValue(value as JSONValue) });
|
||||
}
|
||||
|
||||
return table.toString();
|
||||
}
|
||||
|
||||
function truncateValue(val: JSONValue, maxLen: number = 60): string {
|
||||
if (val === null || val === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof val === "object") {
|
||||
const str: string = JSON.stringify(val);
|
||||
if (str.length > maxLen) {
|
||||
return str.substring(0, maxLen - 3) + "...";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const str: string = String(val);
|
||||
if (str.length > maxLen) {
|
||||
return str.substring(0, maxLen - 3) + "...";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function formatOutput(data: JSONValue, format?: string): string {
|
||||
const outputFormat: OutputFormat = detectOutputFormat(format);
|
||||
|
||||
switch (outputFormat) {
|
||||
case OutputFormat.JSON:
|
||||
return formatJson(data);
|
||||
case OutputFormat.Wide:
|
||||
return formatTable(data, true);
|
||||
case OutputFormat.Table:
|
||||
default:
|
||||
return formatTable(data, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function printSuccess(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function printError(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function printWarning(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.yellow(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function printInfo(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.blue(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
27
CLI/Index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env npx ts-node
|
||||
|
||||
import { Command } from "commander";
|
||||
import { registerConfigCommands } from "./Commands/ConfigCommands";
|
||||
import { registerResourceCommands } from "./Commands/ResourceCommands";
|
||||
import { registerUtilityCommands } from "./Commands/UtilityCommands";
|
||||
|
||||
const program: Command = new Command();
|
||||
|
||||
program
|
||||
.name("oneuptime")
|
||||
.description(
|
||||
"OneUptime CLI - Manage your OneUptime resources from the command line",
|
||||
)
|
||||
.version("1.0.0")
|
||||
.option("--api-key <key>", "API key (overrides config)")
|
||||
.option("--url <url>", "OneUptime instance URL (overrides config)")
|
||||
.option("--context <name>", "Use a specific context")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.option("--no-color", "Disable colored output");
|
||||
|
||||
// Register command groups
|
||||
registerConfigCommands(program);
|
||||
registerUtilityCommands(program);
|
||||
registerResourceCommands(program);
|
||||
|
||||
program.parse(process.argv);
|
||||
220
CLI/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# @oneuptime/cli
|
||||
|
||||
Command-line interface for managing OneUptime resources. Supports all MCP-enabled resources with full CRUD operations, named contexts for multiple environments, and flexible output formats.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @oneuptime/cli
|
||||
```
|
||||
|
||||
Or run directly within the monorepo:
|
||||
|
||||
```bash
|
||||
cd CLI
|
||||
npm install
|
||||
npm start -- --help
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate with your OneUptime instance
|
||||
oneuptime login <api-key> <instance-url>
|
||||
oneuptime login sk-your-api-key https://oneuptime.com
|
||||
|
||||
# List incidents
|
||||
oneuptime incident list --limit 10
|
||||
|
||||
# Get a single resource by ID
|
||||
oneuptime monitor get 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
# Create a resource
|
||||
oneuptime monitor create --data '{"name":"API Health","projectId":"..."}'
|
||||
|
||||
# See all available resources
|
||||
oneuptime resources
|
||||
```
|
||||
|
||||
## Authentication & Contexts
|
||||
|
||||
The CLI supports multiple authentication contexts, making it easy to switch between environments.
|
||||
|
||||
### Setting Up
|
||||
|
||||
```bash
|
||||
# Create a production context
|
||||
oneuptime login sk-prod-key https://oneuptime.com --context-name production
|
||||
|
||||
# Create a staging context
|
||||
oneuptime login sk-staging-key https://staging.oneuptime.com --context-name staging
|
||||
```
|
||||
|
||||
### Switching Contexts
|
||||
|
||||
```bash
|
||||
# List all contexts
|
||||
oneuptime context list
|
||||
|
||||
# Switch active context
|
||||
oneuptime context use staging
|
||||
|
||||
# Show current context
|
||||
oneuptime context current
|
||||
|
||||
# Delete a context
|
||||
oneuptime context delete old-context
|
||||
```
|
||||
|
||||
### Credential Resolution Order
|
||||
|
||||
1. CLI flags: `--api-key` and `--url`
|
||||
2. Environment variables: `ONEUPTIME_API_KEY` and `ONEUPTIME_URL`
|
||||
3. Current context from config file (`~/.oneuptime/config.json`)
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Authentication
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `oneuptime login <api-key> <url>` | Authenticate and create a context |
|
||||
| `oneuptime context list` | List all contexts |
|
||||
| `oneuptime context use <name>` | Switch active context |
|
||||
| `oneuptime context current` | Show current context |
|
||||
| `oneuptime context delete <name>` | Remove a context |
|
||||
| `oneuptime whoami` | Show current auth info |
|
||||
|
||||
### Resource Operations
|
||||
|
||||
Every discovered resource supports these subcommands:
|
||||
|
||||
| Subcommand | Description |
|
||||
|---|---|
|
||||
| `<resource> list [options]` | List resources with filtering and pagination |
|
||||
| `<resource> get <id>` | Get a single resource by ID |
|
||||
| `<resource> create --data <json>` | Create a new resource |
|
||||
| `<resource> update <id> --data <json>` | Update an existing resource |
|
||||
| `<resource> delete <id>` | Delete a resource |
|
||||
| `<resource> count [--query <json>]` | Count resources |
|
||||
|
||||
### List Options
|
||||
|
||||
```
|
||||
--query <json> Filter criteria as JSON
|
||||
--limit <n> Maximum number of results (default: 10)
|
||||
--skip <n> Number of results to skip (default: 0)
|
||||
--sort <json> Sort order as JSON (e.g. '{"createdAt": -1}')
|
||||
-o, --output Output format: json, table, wide
|
||||
```
|
||||
|
||||
### Utility Commands
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `oneuptime version` | Print CLI version |
|
||||
| `oneuptime whoami` | Show current authentication info |
|
||||
| `oneuptime resources` | List all available resource types |
|
||||
|
||||
## Output Formats
|
||||
|
||||
| Format | Description |
|
||||
|---|---|
|
||||
| `table` | Formatted ASCII table (default for TTY) |
|
||||
| `json` | Raw JSON (default when piped) |
|
||||
| `wide` | Table with all columns shown |
|
||||
|
||||
```bash
|
||||
# Explicit format
|
||||
oneuptime incident list -o json
|
||||
oneuptime incident list -o table
|
||||
oneuptime incident list -o wide
|
||||
|
||||
# Pipe to jq (auto-detects JSON)
|
||||
oneuptime incident list | jq '.[].title'
|
||||
```
|
||||
|
||||
## Scripting Examples
|
||||
|
||||
```bash
|
||||
# List incidents as JSON for scripting
|
||||
oneuptime incident list -o json --limit 100
|
||||
|
||||
# Count resources with filter
|
||||
oneuptime incident count --query '{"currentIncidentStateId":"..."}'
|
||||
|
||||
# Create from a JSON file
|
||||
oneuptime monitor create --file monitor.json
|
||||
|
||||
# Use environment variables in CI/CD
|
||||
ONEUPTIME_API_KEY=sk-xxx ONEUPTIME_URL=https://oneuptime.com oneuptime incident list
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `ONEUPTIME_API_KEY` | API key for authentication |
|
||||
| `ONEUPTIME_URL` | OneUptime instance URL |
|
||||
| `NO_COLOR` | Disable colored output |
|
||||
|
||||
## Configuration File
|
||||
|
||||
The CLI stores configuration at `~/.oneuptime/config.json` with `0600` permissions. The file contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"currentContext": "production",
|
||||
"contexts": {
|
||||
"production": {
|
||||
"name": "production",
|
||||
"apiUrl": "https://oneuptime.com",
|
||||
"apiKey": "sk-..."
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"output": "table",
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `--api-key <key>` | Override API key for this command |
|
||||
| `--url <url>` | Override instance URL for this command |
|
||||
| `--context <name>` | Use a specific context for this command |
|
||||
| `-o, --output <format>` | Output format: json, table, wide |
|
||||
| `--no-color` | Disable colored output |
|
||||
|
||||
## Supported Resources
|
||||
|
||||
Run `oneuptime resources` to see all available resource types. Resources are auto-discovered from OneUptime models that have MCP enabled. Currently supported:
|
||||
|
||||
- **Incident** - Manage incidents
|
||||
- **Alert** - Manage alerts
|
||||
- **Monitor** - Manage monitors
|
||||
- **Monitor Status** - Manage monitor statuses
|
||||
- **Incident State** - Manage incident states
|
||||
- **Status Page** - Manage status pages
|
||||
- **On-Call Policy** - Manage on-call duty policies
|
||||
- **Team** - Manage teams
|
||||
- **Scheduled Maintenance Event** - Manage scheduled maintenance
|
||||
|
||||
As more models are MCP-enabled in OneUptime, they automatically become available in the CLI.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd CLI
|
||||
npm install
|
||||
npm start -- --help # Run via ts-node
|
||||
npm test # Run tests
|
||||
npm run compile # Type-check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
386
CLI/Tests/ApiClient.test.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
import { executeApiRequest, ApiRequestOptions } from "../Core/ApiClient";
|
||||
import API from "Common/Utils/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import { JSONValue } from "Common/Types/JSON";
|
||||
|
||||
// Mock the Common/Utils/API module
|
||||
jest.mock("Common/Utils/API", () => {
|
||||
const mockPost: jest.Mock = jest.fn();
|
||||
const mockPut: jest.Mock = jest.fn();
|
||||
const mockDelete: jest.Mock = jest.fn();
|
||||
|
||||
function MockAPI(
|
||||
this: { protocol: string; hostname: string },
|
||||
protocol: string,
|
||||
hostname: string,
|
||||
_route: string,
|
||||
): void {
|
||||
this.protocol = protocol;
|
||||
this.hostname = hostname;
|
||||
}
|
||||
|
||||
MockAPI.post = mockPost;
|
||||
MockAPI.put = mockPut;
|
||||
MockAPI.delete = mockDelete;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: MockAPI,
|
||||
};
|
||||
});
|
||||
|
||||
function createSuccessResponse(
|
||||
data: Record<string, unknown> | Record<string, unknown>[],
|
||||
): {
|
||||
data: Record<string, unknown> | Record<string, unknown>[];
|
||||
statusCode: number;
|
||||
} {
|
||||
return { data, statusCode: 200 };
|
||||
}
|
||||
|
||||
function createErrorResponse(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
): HTTPErrorResponse {
|
||||
/*
|
||||
* HTTPErrorResponse computes `message` from `.data` via a getter.
|
||||
* We create a proper prototype chain and set data to contain the message.
|
||||
*/
|
||||
const resp: HTTPErrorResponse = Object.create(HTTPErrorResponse.prototype);
|
||||
resp.statusCode = statusCode;
|
||||
/*
|
||||
* HTTPResponse stores data in _jsonData and exposes it via `data` getter
|
||||
* But since the prototype chain may not have full getters, we define them
|
||||
*/
|
||||
Object.defineProperty(resp, "data", {
|
||||
get: (): { message: string } => {
|
||||
return { message: message };
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
describe("ApiClient", () => {
|
||||
let mockPost: jest.Mock;
|
||||
let mockPut: jest.Mock;
|
||||
let mockDelete: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPost = API.post as jest.Mock;
|
||||
mockPut = API.put as jest.Mock;
|
||||
mockDelete = API.delete as jest.Mock;
|
||||
(mockPost as jest.Mock).mockReset();
|
||||
(mockPut as jest.Mock).mockReset();
|
||||
(mockDelete as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
const baseOptions: ApiRequestOptions = {
|
||||
apiUrl: "https://oneuptime.com",
|
||||
apiKey: "test-api-key",
|
||||
apiPath: "/incident",
|
||||
operation: "create",
|
||||
};
|
||||
|
||||
describe("create operation", () => {
|
||||
it("should make a POST request with data wrapped in { data: ... }", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "123" }),
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "create",
|
||||
data: { name: "Test Incident" },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: { name: "Test Incident" } });
|
||||
expect(result).toEqual({ _id: "123" });
|
||||
});
|
||||
|
||||
it("should use empty object when no data provided for create", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "123" }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "create",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("read operation", () => {
|
||||
it("should make a POST request with select and id in route", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "abc", name: "Test" }),
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "read",
|
||||
id: "abc-123",
|
||||
select: { _id: true, name: true },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123/get-item");
|
||||
expect(callArgs.data).toEqual({ select: { _id: true, name: true } });
|
||||
expect(result).toEqual({ _id: "abc", name: "Test" });
|
||||
});
|
||||
|
||||
it("should use empty select when none provided", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "read",
|
||||
id: "abc-123",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ select: {} });
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "read",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
expect(callArgs.url.toString()).not.toContain("/get-item");
|
||||
});
|
||||
});
|
||||
|
||||
describe("list operation", () => {
|
||||
it("should make a POST request with query, select, skip, limit, sort", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ data: [] }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "list",
|
||||
query: { status: "active" },
|
||||
select: { _id: true },
|
||||
skip: 5,
|
||||
limit: 20,
|
||||
sort: { createdAt: -1 },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/get-list");
|
||||
expect(callArgs.data).toEqual({
|
||||
query: { status: "active" },
|
||||
select: { _id: true },
|
||||
skip: 5,
|
||||
limit: 20,
|
||||
sort: { createdAt: -1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("should use defaults when no query options provided", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ data: [] }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "list",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({
|
||||
query: {},
|
||||
select: {},
|
||||
skip: 0,
|
||||
limit: 10,
|
||||
sort: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("count operation", () => {
|
||||
it("should make a POST request to /count path", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ count: 42 }),
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "count",
|
||||
query: { status: "active" },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/count");
|
||||
expect(result).toEqual({ count: 42 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("update operation", () => {
|
||||
it("should make a PUT request with data", async () => {
|
||||
(mockPut as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "abc" }),
|
||||
);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "update",
|
||||
id: "abc-123",
|
||||
data: { name: "Updated" },
|
||||
});
|
||||
|
||||
expect(mockPut).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123");
|
||||
expect(callArgs.data).toEqual({ data: { name: "Updated" } });
|
||||
expect(result).toEqual({ _id: "abc" });
|
||||
});
|
||||
|
||||
it("should use empty object when no data provided for update", async () => {
|
||||
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "update",
|
||||
id: "abc-123",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: {} });
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "update",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete operation", () => {
|
||||
it("should make a DELETE request", async () => {
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "delete",
|
||||
id: "abc-123",
|
||||
});
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123");
|
||||
expect(callArgs.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "delete",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should throw on HTTPErrorResponse", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createErrorResponse(500, "Server Error"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "create", data: {} }),
|
||||
).rejects.toThrow("API error");
|
||||
});
|
||||
|
||||
it("should include status code in error message", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createErrorResponse(403, "Forbidden"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "list" }),
|
||||
).rejects.toThrow("403");
|
||||
});
|
||||
|
||||
it("should handle error response with no message", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(createErrorResponse(500, ""));
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "list" }),
|
||||
).rejects.toThrow("API error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("headers", () => {
|
||||
it("should include APIKey, Content-Type, and Accept headers", async () => {
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "create",
|
||||
data: { name: "Test" },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.headers["APIKey"]).toBe("test-api-key");
|
||||
expect(callArgs.headers["Content-Type"]).toBe("application/json");
|
||||
expect(callArgs.headers["Accept"]).toBe("application/json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("default/unknown operation", () => {
|
||||
it("should handle unknown operation in buildRequestData (falls to default)", async () => {
|
||||
// The "delete" case hits the default branch in buildRequestData returning undefined
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "delete",
|
||||
id: "123",
|
||||
});
|
||||
|
||||
// Should not send data for delete
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
257
CLI/Tests/ConfigCommands.test.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { Command } from "commander";
|
||||
import { registerConfigCommands } from "../Commands/ConfigCommands";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ConfigCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
let consoleLogSpy: jest.SpyInstance;
|
||||
let exitSpy: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalConfigContent) {
|
||||
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
|
||||
} else if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createProgram(): Command {
|
||||
const program: Command = new Command();
|
||||
program.exitOverride(); // Prevent commander from calling process.exit
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
writeErr: () => {},
|
||||
});
|
||||
registerConfigCommands(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe("login command", () => {
|
||||
it("should create a context and set it as current", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"login",
|
||||
"my-api-key",
|
||||
"https://example.com",
|
||||
]);
|
||||
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx!.name).toBe("default");
|
||||
expect(ctx!.apiUrl).toBe("https://example.com");
|
||||
expect(ctx!.apiKey).toBe("my-api-key");
|
||||
});
|
||||
|
||||
it("should use custom context name", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"login",
|
||||
"key123",
|
||||
"https://prod.com",
|
||||
"--context-name",
|
||||
"production",
|
||||
]);
|
||||
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx!.name).toBe("production");
|
||||
});
|
||||
|
||||
it("should handle login errors gracefully", async () => {
|
||||
// Mock addContext to throw
|
||||
const addCtxSpy: jest.SpyInstance = jest
|
||||
.spyOn(ConfigManager, "addContext")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("Permission denied");
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"login",
|
||||
"key123",
|
||||
"https://example.com",
|
||||
]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
addCtxSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should strip trailing slashes from URL", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"login",
|
||||
"key123",
|
||||
"https://example.com///",
|
||||
]);
|
||||
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx!.apiUrl).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("context list command", () => {
|
||||
it("should show message when no contexts exist", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "list"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should list contexts with current marker", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "list"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("context use command", () => {
|
||||
it("should switch to the specified context", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "use", "b"]);
|
||||
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
|
||||
it("should handle non-existent context", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "use", "nope"]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("context current command", () => {
|
||||
it("should show current context info", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "myctx",
|
||||
apiUrl: "https://myctx.com",
|
||||
apiKey: "abcdefghijklm",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
|
||||
// Check that masked key is shown
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Context: myctx");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://myctx.com");
|
||||
// Key should be masked: abcd****jklm
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("****"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should show message when no current context", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should mask short API keys", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "short",
|
||||
apiUrl: "https://s.com",
|
||||
apiKey: "abc",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
|
||||
});
|
||||
});
|
||||
|
||||
describe("context delete command", () => {
|
||||
it("should delete a context", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "todelete",
|
||||
apiUrl: "https://del.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"context",
|
||||
"delete",
|
||||
"todelete",
|
||||
]);
|
||||
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should handle deletion of non-existent context", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"context",
|
||||
"delete",
|
||||
"nonexistent",
|
||||
]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
446
CLI/Tests/ConfigManager.test.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import { CLIConfig, ResolvedCredentials } from "../Types/CLITypes";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ConfigManager", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalConfigContent) {
|
||||
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
|
||||
} else if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
describe("load", () => {
|
||||
it("should return default config when no config file exists", () => {
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
expect(config.contexts).toEqual({});
|
||||
expect(config.defaults.output).toBe("table");
|
||||
expect(config.defaults.limit).toBe(10);
|
||||
});
|
||||
|
||||
it("should load existing config from file", () => {
|
||||
const testConfig: CLIConfig = {
|
||||
currentContext: "test",
|
||||
contexts: {
|
||||
test: { name: "test", apiUrl: "https://test.com", apiKey: "key123" },
|
||||
},
|
||||
defaults: { output: "json", limit: 20 },
|
||||
};
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(testConfig), {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("test");
|
||||
expect(config.contexts["test"]?.apiKey).toBe("key123");
|
||||
});
|
||||
|
||||
it("should return default config when file contains invalid JSON", () => {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, "not valid json {{{", { mode: 0o600 });
|
||||
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
expect(config.contexts).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
it("should create config directory if it does not exist", () => {
|
||||
// Remove the dir if it exists (we'll restore after)
|
||||
const tmpDir: string = path.join(
|
||||
os.tmpdir(),
|
||||
".oneuptime-test-" + Date.now(),
|
||||
);
|
||||
/*
|
||||
* We can't easily test this with the real path, but we verify save works
|
||||
* when the dir already exists (which it does after beforeAll).
|
||||
*/
|
||||
const config: CLIConfig = {
|
||||
currentContext: "",
|
||||
contexts: {},
|
||||
defaults: { output: "table", limit: 10 },
|
||||
};
|
||||
ConfigManager.save(config);
|
||||
expect(fs.existsSync(CONFIG_FILE)).toBe(true);
|
||||
void tmpDir; // unused but shows intent
|
||||
});
|
||||
|
||||
it("should write config with correct permissions", () => {
|
||||
const config: CLIConfig = {
|
||||
currentContext: "x",
|
||||
contexts: {
|
||||
x: { name: "x", apiUrl: "https://x.com", apiKey: "k" },
|
||||
},
|
||||
defaults: { output: "table", limit: 10 },
|
||||
};
|
||||
ConfigManager.save(config);
|
||||
const content: string = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
const parsed: CLIConfig = JSON.parse(content);
|
||||
expect(parsed.currentContext).toBe("x");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurrentContext", () => {
|
||||
it("should return null when no current context is set", () => {
|
||||
expect(ConfigManager.getCurrentContext()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when currentContext name does not match any context", () => {
|
||||
// Manually write a config with a dangling currentContext reference
|
||||
const config: CLIConfig = {
|
||||
currentContext: "ghost",
|
||||
contexts: {},
|
||||
defaults: { output: "table", limit: 10 },
|
||||
};
|
||||
ConfigManager.save(config);
|
||||
expect(ConfigManager.getCurrentContext()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the current context when set", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "prod",
|
||||
apiUrl: "https://prod.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx!.name).toBe("prod");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addContext", () => {
|
||||
it("should add a context and set it as current if first context", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "prod",
|
||||
apiUrl: "https://prod.oneuptime.com",
|
||||
apiKey: "sk-prod-123",
|
||||
});
|
||||
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current).not.toBeNull();
|
||||
expect(current!.name).toBe("prod");
|
||||
});
|
||||
|
||||
it("should not change current context when adding a second context", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "prod",
|
||||
apiUrl: "https://prod.com",
|
||||
apiKey: "key1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "staging",
|
||||
apiUrl: "https://staging.com",
|
||||
apiKey: "key2",
|
||||
});
|
||||
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("prod"); // First one remains current
|
||||
});
|
||||
|
||||
it("should add multiple contexts", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "prod",
|
||||
apiUrl: "https://prod.com",
|
||||
apiKey: "key1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "staging",
|
||||
apiUrl: "https://staging.com",
|
||||
apiKey: "key2",
|
||||
});
|
||||
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCurrentContext", () => {
|
||||
it("should switch the active context", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
|
||||
ConfigManager.setCurrentContext("b");
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
|
||||
it("should throw for non-existent context", () => {
|
||||
expect(() => {
|
||||
return ConfigManager.setCurrentContext("nonexistent");
|
||||
}).toThrow('Context "nonexistent" does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeContext", () => {
|
||||
it("should remove a context", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "test",
|
||||
apiUrl: "https://test.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.removeContext("test");
|
||||
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should throw for non-existent context", () => {
|
||||
expect(() => {
|
||||
return ConfigManager.removeContext("nonexistent");
|
||||
}).toThrow('Context "nonexistent" does not exist');
|
||||
});
|
||||
|
||||
it("should update current context when removing the current one", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
ConfigManager.setCurrentContext("a");
|
||||
ConfigManager.removeContext("a");
|
||||
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current).not.toBeNull();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
|
||||
it("should set current context to empty when removing last context", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "only",
|
||||
apiUrl: "https://only.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.removeContext("only");
|
||||
|
||||
expect(ConfigManager.getCurrentContext()).toBeNull();
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
});
|
||||
|
||||
it("should not change current context when removing a non-current one", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
ConfigManager.setCurrentContext("a");
|
||||
ConfigManager.removeContext("b");
|
||||
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("listContexts", () => {
|
||||
it("should return empty array when no contexts exist", () => {
|
||||
expect(ConfigManager.listContexts()).toEqual([]);
|
||||
});
|
||||
|
||||
it("should mark current context correctly", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "a",
|
||||
apiUrl: "https://a.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
ConfigManager.addContext({
|
||||
name: "b",
|
||||
apiUrl: "https://b.com",
|
||||
apiKey: "k2",
|
||||
});
|
||||
ConfigManager.setCurrentContext("b");
|
||||
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
const a:
|
||||
| ReturnType<typeof ConfigManager.listContexts>[number]
|
||||
| undefined = contexts.find(
|
||||
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
|
||||
return c.name === "a";
|
||||
},
|
||||
);
|
||||
const b:
|
||||
| ReturnType<typeof ConfigManager.listContexts>[number]
|
||||
| undefined = contexts.find(
|
||||
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
|
||||
return c.name === "b";
|
||||
},
|
||||
);
|
||||
expect(a!.isCurrent).toBe(false);
|
||||
expect(b!.isCurrent).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResolvedCredentials", () => {
|
||||
it("should resolve from CLI options first", () => {
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
|
||||
apiKey: "cli-key",
|
||||
url: "https://cli.com",
|
||||
});
|
||||
expect(creds.apiKey).toBe("cli-key");
|
||||
expect(creds.apiUrl).toBe("https://cli.com");
|
||||
});
|
||||
|
||||
it("should resolve from env vars when CLI options are missing", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
expect(creds.apiKey).toBe("env-key");
|
||||
expect(creds.apiUrl).toBe("https://env.com");
|
||||
});
|
||||
|
||||
it("should resolve from --context flag", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "named",
|
||||
apiUrl: "https://named.com",
|
||||
apiKey: "named-key",
|
||||
});
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
|
||||
context: "named",
|
||||
});
|
||||
expect(creds.apiKey).toBe("named-key");
|
||||
expect(creds.apiUrl).toBe("https://named.com");
|
||||
});
|
||||
|
||||
it("should throw when --context flag references non-existent context", () => {
|
||||
expect(() => {
|
||||
return ConfigManager.getResolvedCredentials({ context: "nope" });
|
||||
}).toThrow('Context "nope" does not exist');
|
||||
});
|
||||
|
||||
it("should resolve from current context in config", () => {
|
||||
ConfigManager.addContext({
|
||||
name: "ctx",
|
||||
apiUrl: "https://ctx.com",
|
||||
apiKey: "ctx-key",
|
||||
});
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
expect(creds.apiKey).toBe("ctx-key");
|
||||
expect(creds.apiUrl).toBe("https://ctx.com");
|
||||
});
|
||||
|
||||
it("should resolve from partial env vars (only ONEUPTIME_API_KEY)", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "partial-key";
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
expect(creds.apiKey).toBe("partial-key");
|
||||
expect(creds.apiUrl).toBe("");
|
||||
});
|
||||
|
||||
it("should resolve from partial env vars (only ONEUPTIME_URL)", () => {
|
||||
process.env["ONEUPTIME_URL"] = "https://partial.com";
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
expect(creds.apiKey).toBe("");
|
||||
expect(creds.apiUrl).toBe("https://partial.com");
|
||||
});
|
||||
|
||||
it("should combine partial env var with context", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key";
|
||||
ConfigManager.addContext({
|
||||
name: "ctx",
|
||||
apiUrl: "https://ctx.com",
|
||||
apiKey: "ctx-key",
|
||||
});
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
/*
|
||||
* env vars take priority: both are set so goes through priority 2
|
||||
* Actually, only ONEUPTIME_API_KEY is set, not ONEUPTIME_URL
|
||||
* So it falls through to priority 4 (current context)
|
||||
*/
|
||||
expect(creds.apiKey).toBe("ctx-key");
|
||||
expect(creds.apiUrl).toBe("https://ctx.com");
|
||||
});
|
||||
|
||||
it("should throw when no credentials available at all", () => {
|
||||
expect(() => {
|
||||
return ConfigManager.getResolvedCredentials({});
|
||||
}).toThrow("No credentials found");
|
||||
});
|
||||
|
||||
it("should prefer CLI flags over env vars", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
|
||||
apiKey: "cli-key",
|
||||
url: "https://cli.com",
|
||||
});
|
||||
expect(creds.apiKey).toBe("cli-key");
|
||||
expect(creds.apiUrl).toBe("https://cli.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
98
CLI/Tests/ErrorHandler.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { handleError, ExitCode } from "../Core/ErrorHandler";
|
||||
import * as OutputFormatter from "../Core/OutputFormatter";
|
||||
|
||||
describe("ErrorHandler", () => {
|
||||
let exitSpy: jest.SpyInstance;
|
||||
let printErrorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {
|
||||
// no-op
|
||||
}) as any);
|
||||
printErrorSpy = jest
|
||||
.spyOn(OutputFormatter, "printError")
|
||||
.mockImplementation(() => {
|
||||
// no-op
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
exitSpy.mockRestore();
|
||||
printErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should exit with AuthError for API key errors", () => {
|
||||
handleError(new Error("Invalid API key provided"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith(
|
||||
"Authentication error: Invalid API key provided",
|
||||
);
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
|
||||
});
|
||||
|
||||
it("should exit with AuthError for credentials errors", () => {
|
||||
handleError(new Error("No credentials found"));
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
|
||||
});
|
||||
|
||||
it("should exit with AuthError for Unauthorized errors", () => {
|
||||
handleError(new Error("Unauthorized access"));
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
|
||||
});
|
||||
|
||||
it("should exit with AuthError for 401 errors", () => {
|
||||
handleError(new Error("HTTP 401 response"));
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
|
||||
});
|
||||
|
||||
it("should exit with NotFound for 404 errors", () => {
|
||||
handleError(new Error("HTTP 404 response"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Not found: HTTP 404 response");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound);
|
||||
});
|
||||
|
||||
it("should exit with NotFound for not found errors", () => {
|
||||
handleError(new Error("Resource not found"));
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound);
|
||||
});
|
||||
|
||||
it("should exit with GeneralError for API error messages", () => {
|
||||
handleError(new Error("API error (500): Internal Server Error"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith(
|
||||
"API error (500): Internal Server Error",
|
||||
);
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
it("should exit with GeneralError for generic Error objects", () => {
|
||||
handleError(new Error("Something went wrong"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Error: Something went wrong");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
it("should handle non-Error objects", () => {
|
||||
handleError("string error");
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Error: string error");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
it("should handle null error", () => {
|
||||
handleError(null);
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Error: null");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
it("should handle number error", () => {
|
||||
handleError(42);
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Error: 42");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
describe("ExitCode enum", () => {
|
||||
it("should have correct values", () => {
|
||||
expect(ExitCode.Success).toBe(0);
|
||||
expect(ExitCode.GeneralError).toBe(1);
|
||||
expect(ExitCode.AuthError).toBe(2);
|
||||
expect(ExitCode.NotFound).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
66
CLI/Tests/Index.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Command, Option } from "commander";
|
||||
import { registerConfigCommands } from "../Commands/ConfigCommands";
|
||||
import { registerResourceCommands } from "../Commands/ResourceCommands";
|
||||
import { registerUtilityCommands } from "../Commands/UtilityCommands";
|
||||
|
||||
describe("Index (CLI entry point)", () => {
|
||||
it("should create a program with all command groups registered", () => {
|
||||
const program: Command = new Command();
|
||||
program
|
||||
.name("oneuptime")
|
||||
.description(
|
||||
"OneUptime CLI - Manage your OneUptime resources from the command line",
|
||||
)
|
||||
.version("1.0.0")
|
||||
.option("--api-key <key>", "API key (overrides config)")
|
||||
.option("--url <url>", "OneUptime instance URL (overrides config)")
|
||||
.option("--context <name>", "Use a specific context")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.option("--no-color", "Disable colored output");
|
||||
|
||||
registerConfigCommands(program);
|
||||
registerUtilityCommands(program);
|
||||
registerResourceCommands(program);
|
||||
|
||||
// Verify all expected commands are registered
|
||||
const commandNames: string[] = program.commands.map((c: Command) => {
|
||||
return c.name();
|
||||
});
|
||||
expect(commandNames).toContain("login");
|
||||
expect(commandNames).toContain("context");
|
||||
expect(commandNames).toContain("version");
|
||||
expect(commandNames).toContain("whoami");
|
||||
expect(commandNames).toContain("resources");
|
||||
expect(commandNames).toContain("incident");
|
||||
expect(commandNames).toContain("monitor");
|
||||
expect(commandNames).toContain("alert");
|
||||
});
|
||||
|
||||
it("should set correct program name and description", () => {
|
||||
const program: Command = new Command();
|
||||
program.name("oneuptime").description("OneUptime CLI");
|
||||
|
||||
expect(program.name()).toBe("oneuptime");
|
||||
});
|
||||
|
||||
it("should define global options", () => {
|
||||
const program: Command = new Command();
|
||||
program
|
||||
.option("--api-key <key>", "API key")
|
||||
.option("--url <url>", "URL")
|
||||
.option("--context <name>", "Context")
|
||||
.option("-o, --output <format>", "Output format")
|
||||
.option("--no-color", "Disable color");
|
||||
|
||||
// Parse with just the program name - verify options are registered
|
||||
const options: readonly Option[] = program.options;
|
||||
const optionNames: (string | undefined)[] = options.map((o: Option) => {
|
||||
return o.long || o.short;
|
||||
});
|
||||
expect(optionNames).toContain("--api-key");
|
||||
expect(optionNames).toContain("--url");
|
||||
expect(optionNames).toContain("--context");
|
||||
expect(optionNames).toContain("--output");
|
||||
expect(optionNames).toContain("--no-color");
|
||||
});
|
||||
});
|
||||
373
CLI/Tests/OutputFormatter.test.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import {
|
||||
formatOutput,
|
||||
printSuccess,
|
||||
printError,
|
||||
printWarning,
|
||||
printInfo,
|
||||
} from "../Core/OutputFormatter";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
describe("OutputFormatter", () => {
|
||||
let consoleLogSpy: jest.SpyInstance;
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
let originalNoColor: string | undefined;
|
||||
let originalArgv: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
originalNoColor = process.env["NO_COLOR"];
|
||||
originalArgv = [...process.argv];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
if (originalNoColor !== undefined) {
|
||||
process.env["NO_COLOR"] = originalNoColor;
|
||||
} else {
|
||||
delete process.env["NO_COLOR"];
|
||||
}
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
|
||||
describe("formatOutput with JSON format", () => {
|
||||
it("should format single object as JSON", () => {
|
||||
const data: Record<string, string> = { id: "123", name: "Test" };
|
||||
const result: string = formatOutput(data, "json");
|
||||
expect(JSON.parse(result)).toEqual(data);
|
||||
});
|
||||
|
||||
it("should format array as JSON", () => {
|
||||
const data: Record<string, string>[] = [
|
||||
{ id: "1", name: "A" },
|
||||
{ id: "2", name: "B" },
|
||||
];
|
||||
const result: string = formatOutput(data, "json");
|
||||
expect(JSON.parse(result)).toEqual(data);
|
||||
});
|
||||
|
||||
it("should format null as JSON", () => {
|
||||
const result: string = formatOutput(null, "json");
|
||||
expect(result).toBe("null");
|
||||
});
|
||||
|
||||
it("should format number as JSON", () => {
|
||||
const result: string = formatOutput(42, "json");
|
||||
expect(result).toBe("42");
|
||||
});
|
||||
|
||||
it("should format string as JSON", () => {
|
||||
const result: string = formatOutput("hello", "json");
|
||||
expect(result).toBe('"hello"');
|
||||
});
|
||||
|
||||
it("should format boolean as JSON", () => {
|
||||
const result: string = formatOutput(true, "json");
|
||||
expect(result).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOutput with table format", () => {
|
||||
it("should format array as table", () => {
|
||||
const data: Record<string, string>[] = [
|
||||
{ _id: "1", name: "A" },
|
||||
{ _id: "2", name: "B" },
|
||||
];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("A");
|
||||
expect(result).toContain("2");
|
||||
expect(result).toContain("B");
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const result: string = formatOutput([], "table");
|
||||
expect(result).toBe("No results found.");
|
||||
});
|
||||
|
||||
it("should handle single object as key-value table", () => {
|
||||
const data: Record<string, string> = { name: "Test", status: "Active" };
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("Test");
|
||||
expect(result).toContain("Active");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for null in table mode", () => {
|
||||
const result: string = formatOutput(null, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for undefined in table mode", () => {
|
||||
const result: string = formatOutput(undefined as any, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for empty string in table mode", () => {
|
||||
const result: string = formatOutput("" as any, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should fallback to JSON for array of non-objects", () => {
|
||||
const data: string[] = ["a", "b", "c"];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// First item is not an object, so should fallback to JSON
|
||||
expect(result).toContain('"a"');
|
||||
});
|
||||
|
||||
it("should truncate long string values", () => {
|
||||
const longValue: string = "x".repeat(100);
|
||||
const data: Record<string, string>[] = [{ _id: "1", field: longValue }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("...");
|
||||
});
|
||||
|
||||
it("should truncate long object values", () => {
|
||||
const bigObj: Record<string, string> = { a: "x".repeat(80) };
|
||||
const data: JSONObject[] = [{ _id: "1", nested: bigObj }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("...");
|
||||
});
|
||||
|
||||
it("should show short object values without truncation", () => {
|
||||
const smallObj: Record<string, number> = { a: 1 };
|
||||
const data: JSONObject[] = [{ _id: "1", nested: smallObj }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain('{"a":1}');
|
||||
});
|
||||
|
||||
it("should render null values as empty in table", () => {
|
||||
const data: JSONObject[] = [{ _id: "1", value: null }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
});
|
||||
|
||||
it("should render undefined values as empty in table", () => {
|
||||
const data: JSONObject[] = [{ _id: "1", value: undefined }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOutput with wide format", () => {
|
||||
it("should show all columns in wide mode", () => {
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
_id: "1",
|
||||
name: "A",
|
||||
col1: "x",
|
||||
col2: "y",
|
||||
col3: "z",
|
||||
col4: "w",
|
||||
col5: "v",
|
||||
col6: "u",
|
||||
col7: "t",
|
||||
},
|
||||
];
|
||||
const result: string = formatOutput(data, "wide");
|
||||
expect(result).toContain("col7");
|
||||
});
|
||||
|
||||
it("should limit columns in non-wide table mode", () => {
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
_id: "1",
|
||||
name: "A",
|
||||
col1: "x",
|
||||
col2: "y",
|
||||
col3: "z",
|
||||
col4: "w",
|
||||
col5: "v",
|
||||
col6: "u",
|
||||
col7: "t",
|
||||
},
|
||||
];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Table mode should limit to 6 columns, so col7 should not appear
|
||||
expect(result).not.toContain("col7");
|
||||
});
|
||||
|
||||
it("should prioritize common columns in non-wide mode", () => {
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
extra1: "a",
|
||||
extra2: "b",
|
||||
extra3: "c",
|
||||
extra4: "d",
|
||||
extra5: "e",
|
||||
extra6: "f",
|
||||
_id: "1",
|
||||
name: "Test",
|
||||
title: "Title",
|
||||
status: "Active",
|
||||
createdAt: "2024-01-01",
|
||||
updatedAt: "2024-01-02",
|
||||
},
|
||||
];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Priority columns should appear
|
||||
expect(result).toContain("_id");
|
||||
expect(result).toContain("name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("format auto-detection", () => {
|
||||
it("should default to JSON when not a TTY", () => {
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const data: Record<string, string> = { id: "1" };
|
||||
const result: string = formatOutput(data);
|
||||
expect(() => {
|
||||
return JSON.parse(result);
|
||||
}).not.toThrow();
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should default to table when TTY", () => {
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "Test" }];
|
||||
const result: string = formatOutput(data);
|
||||
// Table format contains box-drawing characters
|
||||
expect(result).toContain("\u2500");
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle unknown format string and default to table via TTY check", () => {
|
||||
const data: Record<string, string>[] = [{ _id: "1" }];
|
||||
// "unknown" is not json/table/wide, so cliFormat falls through and TTY detection occurs
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result: string = formatOutput(data, "unknown");
|
||||
expect(result).toContain("\u2500");
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color handling", () => {
|
||||
it("should respect NO_COLOR env variable in table rendering", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Should not contain ANSI color codes
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
});
|
||||
|
||||
it("should respect --no-color argv flag in table rendering", () => {
|
||||
process.argv.push("--no-color");
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
});
|
||||
|
||||
it("should render single object without color when NO_COLOR set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
const data: Record<string, string> = { name: "Test" };
|
||||
const result: string = formatOutput(data, "table");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
expect(result).toContain("name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printSuccess", () => {
|
||||
it("should log success message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
// Remove --no-color from argv if present
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printSuccess("OK");
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log success message without color when NO_COLOR is set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
printSuccess("OK");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("OK");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printError", () => {
|
||||
it("should log error message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printError("fail");
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log error message without color when NO_COLOR is set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
printError("fail");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("fail");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printWarning", () => {
|
||||
it("should log warning message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printWarning("warn");
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log warning message without color when NO_COLOR is set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
printWarning("warn");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("warn");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printInfo", () => {
|
||||
it("should log info message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printInfo("info");
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log info message without color when NO_COLOR is set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
printInfo("info");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("info");
|
||||
});
|
||||
});
|
||||
});
|
||||
568
CLI/Tests/ResourceCommands.test.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import { Command } from "commander";
|
||||
import { ResourceInfo } from "../Types/CLITypes";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
// Mock the ApiClient module before it's imported by ResourceCommands
|
||||
const mockExecuteApiRequest: jest.Mock = jest.fn();
|
||||
jest.mock("../Core/ApiClient", () => {
|
||||
return {
|
||||
...jest.requireActual("../Core/ApiClient"),
|
||||
executeApiRequest: (...args: unknown[]): unknown => {
|
||||
return mockExecuteApiRequest(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mock setup
|
||||
import {
|
||||
discoverResources,
|
||||
registerResourceCommands,
|
||||
} from "../Commands/ResourceCommands";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ResourceCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalConfigContent) {
|
||||
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
|
||||
} else if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
||||
mockExecuteApiRequest.mockReset();
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
describe("discoverResources", () => {
|
||||
let resources: ResourceInfo[];
|
||||
|
||||
beforeAll(() => {
|
||||
resources = discoverResources();
|
||||
});
|
||||
|
||||
it("should discover at least one resource", () => {
|
||||
expect(resources.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should discover the Incident resource", () => {
|
||||
const incident: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Incident";
|
||||
},
|
||||
);
|
||||
expect(incident).toBeDefined();
|
||||
expect(incident!.modelType).toBe("database");
|
||||
expect(incident!.apiPath).toBe("/incident");
|
||||
});
|
||||
|
||||
it("should discover the Monitor resource", () => {
|
||||
const monitor: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Monitor";
|
||||
},
|
||||
);
|
||||
expect(monitor).toBeDefined();
|
||||
expect(monitor!.modelType).toBe("database");
|
||||
});
|
||||
|
||||
it("should discover the Alert resource", () => {
|
||||
const alert: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Alert";
|
||||
},
|
||||
);
|
||||
expect(alert).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have kebab-case names for all resources", () => {
|
||||
for (const r of resources) {
|
||||
expect(r.name).toMatch(/^[a-z][a-z0-9-]*$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("should have apiPath for all resources", () => {
|
||||
for (const r of resources) {
|
||||
expect(r.apiPath).toBeTruthy();
|
||||
expect(r.apiPath.startsWith("/")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should have valid modelType for all resources", () => {
|
||||
for (const r of resources) {
|
||||
expect(["database", "analytics"]).toContain(r.modelType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerResourceCommands", () => {
|
||||
it("should register commands for all discovered resources", () => {
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
registerResourceCommands(program);
|
||||
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
for (const resource of resources) {
|
||||
const cmd: Command | undefined = program.commands.find((c: Command) => {
|
||||
return c.name() === resource.name;
|
||||
});
|
||||
expect(cmd).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("should register list, get, create, update, delete, count subcommands for database resources", () => {
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
registerResourceCommands(program);
|
||||
|
||||
const incidentCmd: Command | undefined = program.commands.find(
|
||||
(c: Command) => {
|
||||
return c.name() === "incident";
|
||||
},
|
||||
);
|
||||
expect(incidentCmd).toBeDefined();
|
||||
|
||||
const subcommands: string[] = incidentCmd!.commands.map((c: Command) => {
|
||||
return c.name();
|
||||
});
|
||||
expect(subcommands).toContain("list");
|
||||
expect(subcommands).toContain("get");
|
||||
expect(subcommands).toContain("create");
|
||||
expect(subcommands).toContain("update");
|
||||
expect(subcommands).toContain("delete");
|
||||
expect(subcommands).toContain("count");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resource command actions", () => {
|
||||
function createProgramWithResources(): Command {
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
writeErr: () => {},
|
||||
});
|
||||
program
|
||||
.option("--api-key <key>", "API key")
|
||||
.option("--url <url>", "URL")
|
||||
.option("--context <name>", "Context");
|
||||
registerResourceCommands(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ConfigManager.addContext({
|
||||
name: "test",
|
||||
apiUrl: "https://test.oneuptime.com",
|
||||
apiKey: "test-key-12345",
|
||||
});
|
||||
mockExecuteApiRequest.mockResolvedValue({ data: [] });
|
||||
});
|
||||
|
||||
describe("list subcommand", () => {
|
||||
it("should call API with list operation", async () => {
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "list"]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("list");
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].apiPath).toBe(
|
||||
"/incident",
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass query, limit, skip, sort options", async () => {
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"list",
|
||||
"--query",
|
||||
'{"status":"active"}',
|
||||
"--limit",
|
||||
"20",
|
||||
"--skip",
|
||||
"5",
|
||||
"--sort",
|
||||
'{"createdAt":-1}',
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.query).toEqual({ status: "active" });
|
||||
expect(opts.limit).toBe(20);
|
||||
expect(opts.skip).toBe(5);
|
||||
expect(opts.sort).toEqual({ createdAt: -1 });
|
||||
});
|
||||
|
||||
it("should extract data array from response object", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({
|
||||
data: [{ _id: "1", name: "Test" }],
|
||||
});
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"list",
|
||||
"-o",
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle response that is already an array", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue([{ _id: "1" }]);
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"list",
|
||||
"-o",
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(
|
||||
new Error("API error (500): Server Error"),
|
||||
);
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "list"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("get subcommand", () => {
|
||||
it("should call API with read operation and id", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({
|
||||
_id: "abc-123",
|
||||
name: "Test",
|
||||
});
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"get",
|
||||
"abc-123",
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("read");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
});
|
||||
|
||||
it("should support output format flag", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"get",
|
||||
"abc-123",
|
||||
"-o",
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle get errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"get",
|
||||
"abc-123",
|
||||
]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("create subcommand", () => {
|
||||
it("should call API with create operation and data", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"create",
|
||||
"--data",
|
||||
'{"name":"New Incident"}',
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("create");
|
||||
expect(opts.data).toEqual({ name: "New Incident" });
|
||||
});
|
||||
|
||||
it("should support reading data from a file", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
|
||||
|
||||
const tmpFile: string = path.join(
|
||||
os.tmpdir(),
|
||||
"cli-test-" + Date.now() + ".json",
|
||||
);
|
||||
fs.writeFileSync(tmpFile, '{"name":"From File"}');
|
||||
|
||||
try {
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"create",
|
||||
"--file",
|
||||
tmpFile,
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].data).toEqual({
|
||||
name: "From File",
|
||||
});
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile);
|
||||
}
|
||||
});
|
||||
|
||||
it("should error when neither --data nor --file is provided", async () => {
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "create"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error on invalid JSON in --data", async () => {
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"create",
|
||||
"--data",
|
||||
"not-json",
|
||||
]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("update subcommand", () => {
|
||||
it("should call API with update operation, id, and data", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"update",
|
||||
"abc-123",
|
||||
"--data",
|
||||
'{"name":"Updated"}',
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("update");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
expect(opts.data).toEqual({ name: "Updated" });
|
||||
});
|
||||
|
||||
it("should handle update errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"update",
|
||||
"abc-123",
|
||||
"--data",
|
||||
'{"name":"x"}',
|
||||
]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete subcommand", () => {
|
||||
it("should call API with delete operation and id", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({});
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"delete",
|
||||
"abc-123",
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("delete");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
});
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"delete",
|
||||
"abc-123",
|
||||
]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("count subcommand", () => {
|
||||
it("should call API with count operation", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ count: 42 });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("count");
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it("should pass query filter", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ count: 5 });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"incident",
|
||||
"count",
|
||||
"--query",
|
||||
'{"status":"active"}',
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].query).toEqual({
|
||||
status: "active",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle response without count field", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue(99);
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith(99);
|
||||
});
|
||||
|
||||
it("should handle non-object response in count", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue("some-string");
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith("some-string");
|
||||
});
|
||||
|
||||
it("should handle count errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("credential resolution in commands", () => {
|
||||
it("should use global --api-key and --url flags", async () => {
|
||||
ConfigManager.removeContext("test");
|
||||
mockExecuteApiRequest.mockResolvedValue({ data: [] });
|
||||
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"--api-key",
|
||||
"global-key",
|
||||
"--url",
|
||||
"https://global.com",
|
||||
"incident",
|
||||
"list",
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].apiKey).toBe(
|
||||
"global-key",
|
||||
);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].apiUrl).toBe(
|
||||
"https://global.com",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
208
CLI/Tests/SelectFieldGenerator.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
describe("SelectFieldGenerator", () => {
|
||||
describe("generateAllFieldsSelect", () => {
|
||||
describe("database models", () => {
|
||||
it("should return fields for a known database model (Incident)", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
// Should have some common fields
|
||||
expect(select).toHaveProperty("_id");
|
||||
});
|
||||
|
||||
it("should return fields for Monitor model", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Monitor",
|
||||
"database",
|
||||
);
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should return default select for unknown database model", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"NonExistentModel12345",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter fields based on access control", () => {
|
||||
// Testing with a real model that has access control
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
// We just verify it returns something reasonable
|
||||
expect(typeof select).toBe("object");
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("analytics models", () => {
|
||||
it("should return default select for known analytics model (LogItem)", () => {
|
||||
// The Log analytics model has tableName "LogItem"
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"LogItem",
|
||||
"analytics",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return default select for unknown analytics model", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"NonExistentAnalytics",
|
||||
"analytics",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should return default select for unknown model type", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"unknown" as any,
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return default select for empty tableName", () => {
|
||||
const select: JSONObject = generateAllFieldsSelect("", "database");
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle outer exception and return default select", () => {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const DatabaseModels: Record<string, unknown> =
|
||||
require("Common/Models/DatabaseModels/Index").default;
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const origFind: unknown = DatabaseModels.find;
|
||||
try {
|
||||
DatabaseModels.find = (): never => {
|
||||
throw new Error("Simulated error");
|
||||
};
|
||||
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
} finally {
|
||||
DatabaseModels.find = origFind;
|
||||
}
|
||||
});
|
||||
|
||||
it("should return default when getTableColumns returns empty", () => {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const tableColumnModule: Record<
|
||||
string,
|
||||
unknown
|
||||
> = require("Common/Types/Database/TableColumn");
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
|
||||
try {
|
||||
tableColumnModule.getTableColumns = (): Record<string, unknown> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
} finally {
|
||||
tableColumnModule.getTableColumns = origGetTableColumns;
|
||||
}
|
||||
});
|
||||
|
||||
it("should return default when all columns are filtered out", () => {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const tableColumnModule: Record<
|
||||
string,
|
||||
unknown
|
||||
> = require("Common/Types/Database/TableColumn");
|
||||
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
|
||||
const DatabaseModels: Record<string, unknown> =
|
||||
require("Common/Models/DatabaseModels/Index").default;
|
||||
const origFind: unknown = DatabaseModels.find;
|
||||
const Permission: Record<string, unknown> =
|
||||
require("Common/Types/Permission").default;
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
|
||||
try {
|
||||
tableColumnModule.getTableColumns = (): Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
> => {
|
||||
return { field1: {}, field2: {} };
|
||||
};
|
||||
|
||||
DatabaseModels.find = (fn: (model: unknown) => boolean): unknown => {
|
||||
function MockModel(this: Record<string, unknown>): void {
|
||||
this.tableName = "MockTable";
|
||||
this.getColumnAccessControlForAllColumns = (): Record<
|
||||
string,
|
||||
unknown
|
||||
> => {
|
||||
return {
|
||||
field1: { read: [Permission.CurrentUser] },
|
||||
field2: { read: [Permission.CurrentUser] },
|
||||
};
|
||||
};
|
||||
}
|
||||
const matches: boolean = fn(MockModel);
|
||||
if (matches) {
|
||||
return MockModel;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"MockTable",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
} finally {
|
||||
DatabaseModels.find = origFind;
|
||||
tableColumnModule.getTableColumns = origGetTableColumns;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
194
CLI/Tests/UtilityCommands.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Command } from "commander";
|
||||
import { registerUtilityCommands } from "../Commands/UtilityCommands";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("UtilityCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
let consoleLogSpy: jest.SpyInstance;
|
||||
let exitSpy: jest.SpyInstance;
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (originalConfigContent) {
|
||||
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
|
||||
} else if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.unlinkSync(CONFIG_FILE);
|
||||
}
|
||||
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
delete process.env["ONEUPTIME_API_KEY"];
|
||||
delete process.env["ONEUPTIME_URL"];
|
||||
});
|
||||
|
||||
function createProgram(): Command {
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
writeErr: () => {},
|
||||
});
|
||||
program
|
||||
.option("--api-key <key>", "API key")
|
||||
.option("--url <url>", "URL")
|
||||
.option("--context <name>", "Context");
|
||||
registerUtilityCommands(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe("version command", () => {
|
||||
it("should print version", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "version"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
// Should print a version string (either from package.json or fallback)
|
||||
const versionArg: string = consoleLogSpy.mock.calls[0][0];
|
||||
expect(typeof versionArg).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("whoami command", () => {
|
||||
it("should show not authenticated when no credentials", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show credentials from current context", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "test",
|
||||
apiUrl: "https://test.com",
|
||||
apiKey: "abcdefghijklm",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://test.com");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("****"),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Context: test");
|
||||
});
|
||||
|
||||
it("should mask short API keys", async () => {
|
||||
ConfigManager.addContext({
|
||||
name: "short",
|
||||
apiUrl: "https://s.com",
|
||||
apiKey: "abc",
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
|
||||
});
|
||||
|
||||
it("should show credentials from env vars", async () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://env.com");
|
||||
});
|
||||
|
||||
it("should handle whoami outer catch block", async () => {
|
||||
// Mock getCurrentContext to throw an unexpected error
|
||||
const spy: jest.SpyInstance = jest
|
||||
.spyOn(ConfigManager, "getCurrentContext")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("Unexpected crash");
|
||||
});
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("should not show context line when no context exists", async () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
// Should NOT have a "Context:" call since no context is set
|
||||
const contextCalls: any[][] = consoleLogSpy.mock.calls.filter(
|
||||
(call: any[]) => {
|
||||
return typeof call[0] === "string" && call[0].startsWith("Context:");
|
||||
},
|
||||
);
|
||||
expect(contextCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resources command", () => {
|
||||
it("should list all resources", async () => {
|
||||
/*
|
||||
* We need registerResourceCommands for discoverResources to work
|
||||
* but discoverResources is imported directly, so it should work
|
||||
*/
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "resources"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
// Should show total count
|
||||
const lastCall: string =
|
||||
consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0];
|
||||
expect(lastCall).toContain("Total:");
|
||||
});
|
||||
|
||||
it("should filter by type", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"resources",
|
||||
"--type",
|
||||
"database",
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show message when filter returns no results", async () => {
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
"resources",
|
||||
"--type",
|
||||
"nonexistent",
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
34
CLI/Types/CLITypes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface CLIContext {
|
||||
name: string;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface CLIConfig {
|
||||
currentContext: string;
|
||||
contexts: Record<string, CLIContext>;
|
||||
defaults: {
|
||||
output: string;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export enum OutputFormat {
|
||||
JSON = "json",
|
||||
Table = "table",
|
||||
Wide = "wide",
|
||||
}
|
||||
|
||||
export interface ResourceInfo {
|
||||
name: string;
|
||||
singularName: string;
|
||||
pluralName: string;
|
||||
apiPath: string;
|
||||
tableName: string;
|
||||
modelType: "database" | "analytics";
|
||||
}
|
||||
|
||||
export interface ResolvedCredentials {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
116
CLI/Utils/SelectFieldGenerator.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import { getTableColumns } from "Common/Types/Database/TableColumn";
|
||||
import Permission from "Common/Types/Permission";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
interface ColumnAccessControl {
|
||||
read?: Permission[];
|
||||
}
|
||||
|
||||
function shouldIncludeField(
|
||||
columnName: string,
|
||||
accessControlForColumns: Record<string, ColumnAccessControl>,
|
||||
): boolean {
|
||||
const accessControl: ColumnAccessControl | undefined =
|
||||
accessControlForColumns[columnName];
|
||||
|
||||
return (
|
||||
!accessControl ||
|
||||
(accessControl.read !== undefined &&
|
||||
accessControl.read.length > 0 &&
|
||||
!(
|
||||
accessControl.read.length === 1 &&
|
||||
accessControl.read[0] === Permission.CurrentUser
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
export function generateAllFieldsSelect(
|
||||
tableName: string,
|
||||
modelType: "database" | "analytics",
|
||||
): JSONObject {
|
||||
try {
|
||||
if (modelType === "database") {
|
||||
const ModelClass: (new () => BaseModel) | undefined = DatabaseModels.find(
|
||||
(Model: new () => BaseModel): boolean => {
|
||||
try {
|
||||
const instance: BaseModel = new Model();
|
||||
return instance.tableName === tableName;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!ModelClass) {
|
||||
return getDefaultSelect();
|
||||
}
|
||||
|
||||
const modelInstance: BaseModel = new ModelClass();
|
||||
const tableColumns: Record<string, unknown> =
|
||||
getTableColumns(modelInstance);
|
||||
const columnNames: string[] = Object.keys(tableColumns);
|
||||
|
||||
if (columnNames.length === 0) {
|
||||
return getDefaultSelect();
|
||||
}
|
||||
|
||||
const accessControlForColumns: Record<string, ColumnAccessControl> =
|
||||
(
|
||||
modelInstance as unknown as {
|
||||
getColumnAccessControlForAllColumns?: () => Record<
|
||||
string,
|
||||
ColumnAccessControl
|
||||
>;
|
||||
}
|
||||
).getColumnAccessControlForAllColumns?.() || {};
|
||||
|
||||
const selectObject: JSONObject = {};
|
||||
for (const columnName of columnNames) {
|
||||
if (shouldIncludeField(columnName, accessControlForColumns)) {
|
||||
selectObject[columnName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(selectObject).length === 0) {
|
||||
return getDefaultSelect();
|
||||
}
|
||||
|
||||
return selectObject;
|
||||
}
|
||||
|
||||
if (modelType === "analytics") {
|
||||
const ModelClass: (new () => AnalyticsBaseModel) | undefined =
|
||||
AnalyticsModels.find((Model: new () => AnalyticsBaseModel): boolean => {
|
||||
try {
|
||||
const instance: AnalyticsBaseModel = new Model();
|
||||
return instance.tableName === tableName;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!ModelClass) {
|
||||
return getDefaultSelect();
|
||||
}
|
||||
|
||||
// For analytics models, just return a basic select
|
||||
return getDefaultSelect();
|
||||
}
|
||||
|
||||
return getDefaultSelect();
|
||||
} catch {
|
||||
return getDefaultSelect();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultSelect(): JSONObject {
|
||||
return {
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
};
|
||||
}
|
||||
35
CLI/jest.config.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
||||
"collectCoverageFrom": [
|
||||
"**/*.ts",
|
||||
"!**/*.d.ts",
|
||||
"!**/node_modules/**",
|
||||
"!**/build/**",
|
||||
"!**/Tests/**",
|
||||
"!Index.ts"
|
||||
],
|
||||
"setupFilesAfterEnv": [],
|
||||
"testTimeout": 30000,
|
||||
"modulePathIgnorePatterns": ["<rootDir>/build/"],
|
||||
"moduleNameMapper": {
|
||||
"^Common/(.*)$": "<rootDir>/../Common/$1"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(@oneuptime)/)"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.ts$": ["ts-jest", {
|
||||
"tsconfig": {
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"module": "commonjs"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
17176
CLI/package-lock.json
generated
Normal file
41
CLI/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@oneuptime/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "OneUptime CLI - Command-line interface for managing OneUptime resources",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OneUptime/oneuptime"
|
||||
},
|
||||
"main": "Index.ts",
|
||||
"bin": {
|
||||
"oneuptime": "./Index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --require ts-node/register Index.ts",
|
||||
"build": "npm run compile",
|
||||
"compile": "tsc",
|
||||
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
|
||||
"dev": "npx nodemon",
|
||||
"audit": "npm audit --audit-level=low",
|
||||
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
|
||||
"test": "jest --passWithNoTests",
|
||||
"link": "npm link"
|
||||
},
|
||||
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"Common": "npm:@oneuptime/common@latest",
|
||||
"commander": "^12.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.21",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
44
CLI/tsconfig.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "node",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"types": ["node", "jest"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./build/dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["Tests", "build", "node_modules", "jest.config.json"]
|
||||
}
|
||||
@@ -58,6 +58,25 @@ export default class GlobalConfig extends GlobalConfigModel {
|
||||
})
|
||||
public disableSignup?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Disable User Project Creation",
|
||||
description: "Only master admins can create projects when enabled.",
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Boolean,
|
||||
nullable: true,
|
||||
default: false,
|
||||
unique: true,
|
||||
})
|
||||
public disableUserProjectCreation?: boolean = undefined;
|
||||
|
||||
// SMTP Settings.
|
||||
|
||||
@ColumnAccessControl({
|
||||
|
||||
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserCallService, {
|
||||
Service as UserCallServiceType,
|
||||
} from "../Services/UserCallService";
|
||||
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import logger from "../Utils/Logger";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserCall from "../../Models/DatabaseModels/UserCall";
|
||||
import UserSMS from "../../Models/DatabaseModels/UserSMS";
|
||||
|
||||
@@ -52,6 +55,7 @@ export default class UserCallAPI extends BaseAPI<
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
projectId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
@@ -95,6 +99,21 @@ export default class UserCallAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
// Create default notification rules for this verified call number
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(item.projectId!.toString()),
|
||||
userId: new ObjectID(item.userId!.toString()),
|
||||
notificationMethod: {
|
||||
userCallId: item.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserEmailService, {
|
||||
Service as UserEmailServiceType,
|
||||
} from "../Services/UserEmailService";
|
||||
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import logger from "../Utils/Logger";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserEmail from "../../Models/DatabaseModels/UserEmail";
|
||||
|
||||
export default class UserEmailAPI extends BaseAPI<
|
||||
@@ -51,6 +54,7 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
projectId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
@@ -94,6 +98,21 @@ export default class UserEmailAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
// Create default notification rules for this verified email
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(item.projectId!.toString()),
|
||||
userId: new ObjectID(item.userId!.toString()),
|
||||
notificationMethod: {
|
||||
userEmailId: item.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -2,8 +2,10 @@ import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserPushService, {
|
||||
Service as UserPushServiceType,
|
||||
} from "../Services/UserPushService";
|
||||
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
|
||||
import PushNotificationService from "../Services/PushNotificationService";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import logger from "../Utils/Logger";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -13,11 +15,23 @@ import {
|
||||
import Response from "../Utils/Response";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
|
||||
import UserPush from "../../Models/DatabaseModels/UserPush";
|
||||
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
||||
|
||||
function getAuthenticatedUserId(req: ExpressRequest): ObjectID {
|
||||
const userId: ObjectID | undefined = (req as OneUptimeRequest)
|
||||
.userAuthorization?.userId;
|
||||
if (!userId) {
|
||||
throw new NotAuthenticatedException(
|
||||
"You must be logged in to perform this action.",
|
||||
);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export default class UserPushAPI extends BaseAPI<
|
||||
UserPush,
|
||||
UserPushServiceType
|
||||
@@ -32,6 +46,8 @@ export default class UserPushAPI extends BaseAPI<
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
const userId: ObjectID = getAuthenticatedUserId(req);
|
||||
|
||||
if (!req.body.deviceToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
@@ -65,7 +81,7 @@ export default class UserPushAPI extends BaseAPI<
|
||||
// Check if device is already registered
|
||||
const existingDevice: UserPush | null = await this.service.findOneBy({
|
||||
query: {
|
||||
userId: (req as OneUptimeRequest).userAuthorization!.userId!,
|
||||
userId: userId,
|
||||
projectId: new ObjectID(req.body.projectId),
|
||||
deviceToken: req.body.deviceToken,
|
||||
},
|
||||
@@ -78,17 +94,18 @@ export default class UserPushAPI extends BaseAPI<
|
||||
});
|
||||
|
||||
if (existingDevice) {
|
||||
// Mark as used and return a specific response indicating device was already registered
|
||||
throw new BadDataException(
|
||||
"This device is already registered for push notifications",
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
new BadDataException(
|
||||
"This device is already registered for push notifications",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Create new device registration
|
||||
const userPush: UserPush = new UserPush();
|
||||
userPush.userId = (
|
||||
req as OneUptimeRequest
|
||||
).userAuthorization!.userId!;
|
||||
userPush.userId = userId;
|
||||
userPush.projectId = new ObjectID(req.body.projectId);
|
||||
userPush.deviceToken = req.body.deviceToken;
|
||||
userPush.deviceType = req.body.deviceType;
|
||||
@@ -102,6 +119,21 @@ export default class UserPushAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
// Create default notification rules for this registered push device
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(req.body.projectId),
|
||||
userId,
|
||||
notificationMethod: {
|
||||
userPushId: savedDevice.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
success: true,
|
||||
deviceId: savedDevice._id!.toString(),
|
||||
@@ -119,6 +151,8 @@ export default class UserPushAPI extends BaseAPI<
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
const userId: ObjectID = getAuthenticatedUserId(req);
|
||||
|
||||
if (!req.body.deviceToken) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
@@ -127,9 +161,6 @@ export default class UserPushAPI extends BaseAPI<
|
||||
);
|
||||
}
|
||||
|
||||
const userId: ObjectID = (req as OneUptimeRequest).userAuthorization!
|
||||
.userId!;
|
||||
|
||||
await this.service.deleteBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
@@ -159,6 +190,8 @@ export default class UserPushAPI extends BaseAPI<
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
const userId: ObjectID = getAuthenticatedUserId(req);
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
@@ -192,10 +225,7 @@ export default class UserPushAPI extends BaseAPI<
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
if (device.userId?.toString() !== userId.toString()) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -264,6 +294,8 @@ export default class UserPushAPI extends BaseAPI<
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
const userId: ObjectID = getAuthenticatedUserId(req);
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
@@ -279,6 +311,7 @@ export default class UserPushAPI extends BaseAPI<
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -291,10 +324,7 @@ export default class UserPushAPI extends BaseAPI<
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
if (device.userId?.toString() !== userId.toString()) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
@@ -304,6 +334,21 @@ export default class UserPushAPI extends BaseAPI<
|
||||
|
||||
await this.service.verifyDevice(device._id!.toString());
|
||||
|
||||
// Create default notification rules for this verified push device
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(device.projectId!.toString()),
|
||||
userId,
|
||||
notificationMethod: {
|
||||
userPushId: device.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
@@ -318,6 +363,8 @@ export default class UserPushAPI extends BaseAPI<
|
||||
try {
|
||||
req = req as OneUptimeRequest;
|
||||
|
||||
const userId: ObjectID = getAuthenticatedUserId(req);
|
||||
|
||||
if (!req.params["deviceId"]) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
@@ -345,10 +392,7 @@ export default class UserPushAPI extends BaseAPI<
|
||||
}
|
||||
|
||||
// Check if the device belongs to the current user
|
||||
if (
|
||||
device.userId?.toString() !==
|
||||
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
|
||||
) {
|
||||
if (device.userId?.toString() !== userId.toString()) {
|
||||
return Response.sendErrorResponse(
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserSMSService, {
|
||||
Service as UserSMSServiceType,
|
||||
} from "../Services/UserSmsService";
|
||||
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import logger from "../Utils/Logger";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserSMS from "../../Models/DatabaseModels/UserSMS";
|
||||
|
||||
export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
|
||||
@@ -48,6 +51,7 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
projectId: true,
|
||||
verificationCode: true,
|
||||
},
|
||||
});
|
||||
@@ -91,6 +95,21 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
|
||||
},
|
||||
});
|
||||
|
||||
// Create default notification rules for this verified SMS
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(item.projectId!.toString()),
|
||||
userId: new ObjectID(item.userId!.toString()),
|
||||
notificationMethod: {
|
||||
userSmsId: item.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import UserWhatsAppService, {
|
||||
Service as UserWhatsAppServiceType,
|
||||
} from "../Services/UserWhatsAppService";
|
||||
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import logger from "../Utils/Logger";
|
||||
import BaseAPI from "./BaseAPI";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
|
||||
|
||||
export default class UserWhatsAppAPI extends BaseAPI<
|
||||
@@ -50,6 +53,7 @@ export default class UserWhatsAppAPI extends BaseAPI<
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
projectId: true,
|
||||
verificationCode: true,
|
||||
isVerified: true,
|
||||
},
|
||||
@@ -100,6 +104,21 @@ export default class UserWhatsAppAPI extends BaseAPI<
|
||||
},
|
||||
});
|
||||
|
||||
// Create default notification rules for this verified WhatsApp number
|
||||
try {
|
||||
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
|
||||
{
|
||||
projectId: new ObjectID(item.projectId!.toString()),
|
||||
userId: new ObjectID(item.userId!.toString()),
|
||||
notificationMethod: {
|
||||
userWhatsAppId: item.id!,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return Response.sendEmptySuccessResponse(req, res);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -80,4 +80,11 @@ export default class DatabaseConfig {
|
||||
"disableSignup",
|
||||
)) as boolean;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public static async shouldDisableUserProjectCreation(): Promise<boolean> {
|
||||
return (await DatabaseConfig.getFromGlobalConfig(
|
||||
"disableUserProjectCreation",
|
||||
)) as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,6 +529,13 @@ export const VapidPrivateKey: string | undefined =
|
||||
export const VapidSubject: string =
|
||||
process.env["VAPID_SUBJECT"] || "mailto:support@oneuptime.com";
|
||||
|
||||
export const ExpoAccessToken: string | undefined =
|
||||
process.env["EXPO_ACCESS_TOKEN"] || undefined;
|
||||
|
||||
export const PushNotificationRelayUrl: string =
|
||||
process.env["PUSH_NOTIFICATION_RELAY_URL"] ||
|
||||
"https://oneuptime.com/api/notification/push-relay/send";
|
||||
|
||||
export const EnterpriseLicenseValidationUrl: URL = URL.fromString(
|
||||
"https://oneuptime.com/api/enterprise-license/validate",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1770834237091 implements MigrationInterface {
|
||||
public name = "MigrationName1770834237091";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD "disableUserProjectCreation" boolean DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_disableUserProjectCreation" UNIQUE ("disableUserProjectCreation")`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_disableUserProjectCreation"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "GlobalConfig" DROP COLUMN "disableUserProjectCreation"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -258,6 +258,7 @@ import { MigrationName1770728946893 } from "./1770728946893-MigrationName";
|
||||
import { MigrationName1770732721195 } from "./1770732721195-MigrationName";
|
||||
import { MigrationName1770833704656 } from "./1770833704656-MigrationName";
|
||||
import { MigrationName1770834237090 } from "./1770834237090-MigrationName";
|
||||
import { MigrationName1770834237091 } from "./1770834237091-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -520,4 +521,5 @@ export default [
|
||||
MigrationName1770732721195,
|
||||
MigrationName1770833704656,
|
||||
MigrationName1770834237090,
|
||||
MigrationName1770834237091,
|
||||
];
|
||||
|
||||
@@ -124,6 +124,7 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
isMasterAdmin: true,
|
||||
companyPhoneNumber: true,
|
||||
companyName: true,
|
||||
utmCampaign: true,
|
||||
@@ -142,6 +143,15 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
throw new BadDataException("User not found.");
|
||||
}
|
||||
|
||||
// Check if project creation is restricted to admins only
|
||||
const shouldDisableProjectCreation: boolean =
|
||||
await DatabaseConfig.shouldDisableUserProjectCreation();
|
||||
if (shouldDisableProjectCreation && !user.isMasterAdmin) {
|
||||
throw new NotAuthorizedException(
|
||||
"Project creation is restricted to admin users only on this OneUptime Server. Please contact your server admin.",
|
||||
);
|
||||
}
|
||||
|
||||
if (IsBillingEnabled) {
|
||||
if (!data.data.paymentProviderPlanId) {
|
||||
throw new BadDataException("Plan required to create the project.");
|
||||
@@ -1313,6 +1323,8 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
paymentProviderSubscriptionId: true,
|
||||
paymentProviderMeteredSubscriptionId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
planName: true,
|
||||
createdByUser: {
|
||||
name: true,
|
||||
email: true,
|
||||
@@ -1341,6 +1353,8 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
let slackMessage: string = `*Project Deleted:*
|
||||
*Project Name:* ${project.name?.toString() || "N/A"}
|
||||
*Project ID:* ${project._id?.toString() || "N/A"}
|
||||
*Project Created Date:* ${project.createdAt ? new Date(project.createdAt).toUTCString() : "N/A"}
|
||||
*Project Plan Name:* ${project.planName?.toString() || "N/A"}
|
||||
`;
|
||||
|
||||
if (subscriptionStatus) {
|
||||
|
||||
@@ -10,9 +10,16 @@ import {
|
||||
VapidPublicKey,
|
||||
VapidPrivateKey,
|
||||
VapidSubject,
|
||||
ExpoAccessToken,
|
||||
PushNotificationRelayUrl,
|
||||
} from "../EnvironmentConfig";
|
||||
import webpush from "web-push";
|
||||
import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
|
||||
import API from "../../Utils/API";
|
||||
import URL from "../../Types/API/URL";
|
||||
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
|
||||
import HTTPResponse from "../../Types/API/HTTPResponse";
|
||||
import { JSONObject } from "../../Types/JSON";
|
||||
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import UserPush from "../../Models/DatabaseModels/UserPush";
|
||||
@@ -43,7 +50,9 @@ export interface PushNotificationOptions {
|
||||
|
||||
export default class PushNotificationService {
|
||||
public static isWebPushInitialized = false;
|
||||
private static expoClient: Expo = new Expo();
|
||||
private static expoClient: Expo = new Expo(
|
||||
ExpoAccessToken ? { accessToken: ExpoAccessToken } : undefined,
|
||||
);
|
||||
|
||||
public static initializeWebPush(): void {
|
||||
if (this.isWebPushInitialized) {
|
||||
@@ -340,20 +349,33 @@ export default class PushNotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
const dataPayload: { [key: string]: string } = {};
|
||||
if (message.data) {
|
||||
for (const key of Object.keys(message.data)) {
|
||||
dataPayload[key] = String(message.data[key]);
|
||||
}
|
||||
}
|
||||
if (message.url || message.clickAction) {
|
||||
dataPayload["url"] = message.url || message.clickAction || "";
|
||||
}
|
||||
|
||||
const channelId: string =
|
||||
deviceType === PushDeviceType.Android ? "oncall_high" : "default";
|
||||
|
||||
// If EXPO_ACCESS_TOKEN is not set, relay through the push notification gateway
|
||||
if (!ExpoAccessToken) {
|
||||
await this.sendViaRelay(
|
||||
expoPushToken,
|
||||
message,
|
||||
dataPayload,
|
||||
channelId,
|
||||
deviceType,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send directly via Expo SDK
|
||||
try {
|
||||
const dataPayload: { [key: string]: string } = {};
|
||||
if (message.data) {
|
||||
for (const key of Object.keys(message.data)) {
|
||||
dataPayload[key] = String(message.data[key]);
|
||||
}
|
||||
}
|
||||
if (message.url || message.clickAction) {
|
||||
dataPayload["url"] = message.url || message.clickAction || "";
|
||||
}
|
||||
|
||||
const channelId: string =
|
||||
deviceType === PushDeviceType.Android ? "oncall_high" : "default";
|
||||
|
||||
const expoPushMessage: ExpoPushMessage = {
|
||||
to: expoPushToken,
|
||||
title: message.title,
|
||||
@@ -403,6 +425,109 @@ export default class PushNotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
private static async sendViaRelay(
|
||||
expoPushToken: string,
|
||||
message: PushNotificationMessage,
|
||||
dataPayload: { [key: string]: string },
|
||||
channelId: string,
|
||||
deviceType: PushDeviceType,
|
||||
): Promise<void> {
|
||||
logger.info(
|
||||
`Sending ${deviceType} push notification via relay: ${PushNotificationRelayUrl}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
|
||||
await API.post<JSONObject>({
|
||||
url: URL.fromString(PushNotificationRelayUrl),
|
||||
data: {
|
||||
to: expoPushToken,
|
||||
title: message.title || "",
|
||||
body: message.body || "",
|
||||
data: dataPayload,
|
||||
sound: "default",
|
||||
priority: "high",
|
||||
channelId: channelId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw new Error(
|
||||
`Push relay error: ${JSON.stringify(response.jsonData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Push notification sent via relay successfully to ${deviceType} device`,
|
||||
);
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Failed to send push notification via relay to ${deviceType} device: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static isValidExpoPushToken(token: string): boolean {
|
||||
return Expo.isExpoPushToken(token);
|
||||
}
|
||||
|
||||
public static hasExpoAccessToken(): boolean {
|
||||
return Boolean(ExpoAccessToken);
|
||||
}
|
||||
|
||||
public static async sendRelayPushNotification(data: {
|
||||
to: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
data?: { [key: string]: string };
|
||||
sound?: string;
|
||||
priority?: string;
|
||||
channelId?: string;
|
||||
}): Promise<void> {
|
||||
if (!ExpoAccessToken) {
|
||||
throw new Error(
|
||||
"Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
|
||||
);
|
||||
}
|
||||
|
||||
const expoPushMessage: ExpoPushMessage = {
|
||||
to: data.to,
|
||||
title: data.title || "",
|
||||
body: data.body || "",
|
||||
data: data.data || {},
|
||||
sound: (data.sound as "default" | null) || "default",
|
||||
priority:
|
||||
(data.priority as "default" | "normal" | "high") || "high",
|
||||
channelId: data.channelId || "default",
|
||||
};
|
||||
|
||||
const tickets: ExpoPushTicket[] =
|
||||
await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
|
||||
|
||||
const ticket: ExpoPushTicket | undefined = tickets[0];
|
||||
|
||||
if (ticket && ticket.status === "error") {
|
||||
const errorTicket: ExpoPushTicket & {
|
||||
message?: string;
|
||||
details?: { error?: string };
|
||||
} = ticket as ExpoPushTicket & {
|
||||
message?: string;
|
||||
details?: { error?: string };
|
||||
};
|
||||
|
||||
logger.error(
|
||||
`Push relay: Expo push notification error: ${errorTicket.message}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to send push notification: ${errorTicket.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Push relay: notification sent successfully to ${data.to}`);
|
||||
}
|
||||
|
||||
public static async sendPushNotificationToUser(
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
|
||||
@@ -69,6 +69,14 @@ import PushNotificationMessage from "../../Types/PushNotification/PushNotificati
|
||||
import logger from "../Utils/Logger";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
|
||||
export interface NotificationMethodDescriptor {
|
||||
userEmailId?: ObjectID;
|
||||
userSmsId?: ObjectID;
|
||||
userCallId?: ObjectID;
|
||||
userWhatsAppId?: ObjectID;
|
||||
userPushId?: ObjectID;
|
||||
}
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
@@ -2207,13 +2215,89 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async addDefaultIncidentNotificationRuleForUser(data: {
|
||||
public async addDefaultNotificationRulesForVerifiedMethod(data: {
|
||||
projectId: ObjectID;
|
||||
userId: ObjectID;
|
||||
userEmail: UserEmail;
|
||||
notificationMethod: NotificationMethodDescriptor;
|
||||
}): Promise<void> {
|
||||
const { projectId, userId, userEmail } = data;
|
||||
const { projectId, userId, notificationMethod } = data;
|
||||
|
||||
await this.createIncidentOnCallRules(projectId, userId, notificationMethod);
|
||||
await this.createAlertOnCallRules(projectId, userId, notificationMethod);
|
||||
await this.createSingleRule(
|
||||
projectId,
|
||||
userId,
|
||||
notificationMethod,
|
||||
NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
||||
);
|
||||
await this.createSingleRule(
|
||||
projectId,
|
||||
userId,
|
||||
notificationMethod,
|
||||
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
||||
);
|
||||
await this.createSingleRule(
|
||||
projectId,
|
||||
userId,
|
||||
notificationMethod,
|
||||
NotificationRuleType.WHEN_USER_GOES_ON_CALL,
|
||||
);
|
||||
await this.createSingleRule(
|
||||
projectId,
|
||||
userId,
|
||||
notificationMethod,
|
||||
NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
|
||||
);
|
||||
}
|
||||
|
||||
private applyNotificationMethod(
|
||||
rule: Model,
|
||||
descriptor: NotificationMethodDescriptor,
|
||||
): void {
|
||||
if (descriptor.userEmailId) {
|
||||
rule.userEmailId = descriptor.userEmailId;
|
||||
}
|
||||
if (descriptor.userSmsId) {
|
||||
rule.userSmsId = descriptor.userSmsId;
|
||||
}
|
||||
if (descriptor.userCallId) {
|
||||
rule.userCallId = descriptor.userCallId;
|
||||
}
|
||||
if (descriptor.userWhatsAppId) {
|
||||
rule.userWhatsAppId = descriptor.userWhatsAppId;
|
||||
}
|
||||
if (descriptor.userPushId) {
|
||||
rule.userPushId = descriptor.userPushId;
|
||||
}
|
||||
}
|
||||
|
||||
private getNotificationMethodQuery(
|
||||
descriptor: NotificationMethodDescriptor,
|
||||
): Record<string, ObjectID> {
|
||||
const query: Record<string, ObjectID> = {};
|
||||
if (descriptor.userEmailId) {
|
||||
query["userEmailId"] = descriptor.userEmailId;
|
||||
}
|
||||
if (descriptor.userSmsId) {
|
||||
query["userSmsId"] = descriptor.userSmsId;
|
||||
}
|
||||
if (descriptor.userCallId) {
|
||||
query["userCallId"] = descriptor.userCallId;
|
||||
}
|
||||
if (descriptor.userWhatsAppId) {
|
||||
query["userWhatsAppId"] = descriptor.userWhatsAppId;
|
||||
}
|
||||
if (descriptor.userPushId) {
|
||||
query["userPushId"] = descriptor.userPushId;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private async createIncidentOnCallRules(
|
||||
projectId: ObjectID,
|
||||
userId: ObjectID,
|
||||
notificationMethod: NotificationMethodDescriptor,
|
||||
): Promise<void> {
|
||||
const incidentSeverities: Array<IncidentSeverity> =
|
||||
await IncidentSeverityService.findBy({
|
||||
query: {
|
||||
@@ -2229,38 +2313,34 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
});
|
||||
|
||||
// create for incident severities.
|
||||
for (const incidentSeverity of incidentSeverities) {
|
||||
//check if this rule already exists.
|
||||
const existingRule: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId,
|
||||
userId,
|
||||
userEmailId: userEmail.id!,
|
||||
...this.getNotificationMethodQuery(notificationMethod),
|
||||
incidentSeverityId: incidentSeverity.id!,
|
||||
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
||||
},
|
||||
} as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRule) {
|
||||
continue; // skip this rule.
|
||||
continue;
|
||||
}
|
||||
|
||||
const notificationRule: Model = new Model();
|
||||
|
||||
notificationRule.projectId = projectId;
|
||||
notificationRule.userId = userId;
|
||||
notificationRule.userEmailId = userEmail.id!;
|
||||
notificationRule.incidentSeverityId = incidentSeverity.id!;
|
||||
notificationRule.notifyAfterMinutes = 0;
|
||||
notificationRule.ruleType =
|
||||
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
|
||||
const rule: Model = new Model();
|
||||
rule.projectId = projectId;
|
||||
rule.userId = userId;
|
||||
this.applyNotificationMethod(rule, notificationMethod);
|
||||
rule.incidentSeverityId = incidentSeverity.id!;
|
||||
rule.notifyAfterMinutes = 0;
|
||||
rule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
|
||||
|
||||
await this.create({
|
||||
data: notificationRule,
|
||||
data: rule,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
@@ -2268,14 +2348,11 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async addDefaultAlertNotificationRulesForUser(data: {
|
||||
projectId: ObjectID;
|
||||
userId: ObjectID;
|
||||
userEmail: UserEmail;
|
||||
}): Promise<void> {
|
||||
const { projectId, userId, userEmail } = data;
|
||||
|
||||
private async createAlertOnCallRules(
|
||||
projectId: ObjectID,
|
||||
userId: ObjectID,
|
||||
notificationMethod: NotificationMethodDescriptor,
|
||||
): Promise<void> {
|
||||
const alertSeverities: Array<AlertSeverity> =
|
||||
await AlertSeverityService.findBy({
|
||||
query: {
|
||||
@@ -2291,37 +2368,34 @@ export class Service extends DatabaseService<Model> {
|
||||
},
|
||||
});
|
||||
|
||||
// create for Alert severities.
|
||||
for (const alertSeverity of alertSeverities) {
|
||||
//check if this rule already exists.
|
||||
const existingRule: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId,
|
||||
userId,
|
||||
userEmailId: userEmail.id!,
|
||||
...this.getNotificationMethodQuery(notificationMethod),
|
||||
alertSeverityId: alertSeverity.id!,
|
||||
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
||||
},
|
||||
} as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRule) {
|
||||
continue; // skip this rule.
|
||||
continue;
|
||||
}
|
||||
|
||||
const notificationRule: Model = new Model();
|
||||
|
||||
notificationRule.projectId = projectId;
|
||||
notificationRule.userId = userId;
|
||||
notificationRule.userEmailId = userEmail.id!;
|
||||
notificationRule.alertSeverityId = alertSeverity.id!;
|
||||
notificationRule.notifyAfterMinutes = 0;
|
||||
notificationRule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_ALERT;
|
||||
const rule: Model = new Model();
|
||||
rule.projectId = projectId;
|
||||
rule.userId = userId;
|
||||
this.applyNotificationMethod(rule, notificationMethod);
|
||||
rule.alertSeverityId = alertSeverity.id!;
|
||||
rule.notifyAfterMinutes = 0;
|
||||
rule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_ALERT;
|
||||
|
||||
await this.create({
|
||||
data: notificationRule,
|
||||
data: rule,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
@@ -2329,6 +2403,43 @@ export class Service extends DatabaseService<Model> {
|
||||
}
|
||||
}
|
||||
|
||||
private async createSingleRule(
|
||||
projectId: ObjectID,
|
||||
userId: ObjectID,
|
||||
notificationMethod: NotificationMethodDescriptor,
|
||||
ruleType: NotificationRuleType,
|
||||
): Promise<void> {
|
||||
const existingRule: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId,
|
||||
userId,
|
||||
...this.getNotificationMethodQuery(notificationMethod),
|
||||
ruleType,
|
||||
} as any,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rule: Model = new Model();
|
||||
rule.projectId = projectId;
|
||||
rule.userId = userId;
|
||||
this.applyNotificationMethod(rule, notificationMethod);
|
||||
rule.notifyAfterMinutes = 0;
|
||||
rule.ruleType = ruleType;
|
||||
|
||||
await this.create({
|
||||
data: rule,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async addDefaultNotificationRuleForUser(
|
||||
projectId: ObjectID,
|
||||
@@ -2361,82 +2472,13 @@ export class Service extends DatabaseService<Model> {
|
||||
});
|
||||
}
|
||||
|
||||
// add default incident rules for user
|
||||
await this.addDefaultIncidentNotificationRuleForUser({
|
||||
await this.addDefaultNotificationRulesForVerifiedMethod({
|
||||
projectId,
|
||||
userId,
|
||||
userEmail,
|
||||
});
|
||||
|
||||
// add default alert rules for user, just like the incident
|
||||
|
||||
await this.addDefaultAlertNotificationRulesForUser({
|
||||
projectId,
|
||||
userId,
|
||||
userEmail,
|
||||
});
|
||||
|
||||
//check if this rule already exists.
|
||||
const existingRuleOnCall: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId,
|
||||
userId,
|
||||
notificationMethod: {
|
||||
userEmailId: userEmail.id!,
|
||||
ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingRuleOnCall) {
|
||||
// on and off call.
|
||||
const onCallRule: Model = new Model();
|
||||
|
||||
onCallRule.projectId = projectId;
|
||||
onCallRule.userId = userId;
|
||||
onCallRule.userEmailId = userEmail.id!;
|
||||
onCallRule.notifyAfterMinutes = 0;
|
||||
onCallRule.ruleType = NotificationRuleType.WHEN_USER_GOES_ON_CALL;
|
||||
|
||||
await this.create({
|
||||
data: onCallRule,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//check if this rule already exists.
|
||||
const existingRuleOffCall: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId,
|
||||
userId,
|
||||
userEmailId: userEmail.id!,
|
||||
ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingRuleOffCall) {
|
||||
// on and off call.
|
||||
const offCallRule: Model = new Model();
|
||||
|
||||
offCallRule.projectId = projectId;
|
||||
offCallRule.userId = userId;
|
||||
offCallRule.userEmailId = userEmail.id!;
|
||||
offCallRule.notifyAfterMinutes = 0;
|
||||
offCallRule.ruleType = NotificationRuleType.WHEN_USER_GOES_OFF_CALL;
|
||||
|
||||
await this.create({
|
||||
data: offCallRule,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Service();
|
||||
|
||||
142
Common/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import DataToProcess from "../DataToProcess";
|
||||
import CompareCriteria from "./CompareCriteria";
|
||||
import {
|
||||
CheckOn,
|
||||
CriteriaFilter,
|
||||
FilterType,
|
||||
} from "../../../../Types/Monitor/CriteriaFilter";
|
||||
import DomainMonitorResponse from "../../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
|
||||
import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
|
||||
import CaptureSpan from "../../Telemetry/CaptureSpan";
|
||||
|
||||
export default class DomainMonitorCriteria {
|
||||
@CaptureSpan()
|
||||
public static async isMonitorInstanceCriteriaFilterMet(input: {
|
||||
dataToProcess: DataToProcess;
|
||||
criteriaFilter: CriteriaFilter;
|
||||
}): Promise<string | null> {
|
||||
let threshold: number | string | undefined | null =
|
||||
input.criteriaFilter.value;
|
||||
|
||||
const dataToProcess: ProbeMonitorResponse =
|
||||
input.dataToProcess as ProbeMonitorResponse;
|
||||
|
||||
const domainResponse: DomainMonitorResponse | undefined =
|
||||
dataToProcess.domainResponse;
|
||||
|
||||
// Check domain expires in days
|
||||
if (input.criteriaFilter.checkOn === CheckOn.DomainExpiresDaysIn) {
|
||||
threshold = CompareCriteria.convertToNumber(threshold);
|
||||
|
||||
if (threshold === null || threshold === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!domainResponse?.expiresDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresDate: Date = new Date(domainResponse.expiresDate);
|
||||
const now: Date = new Date();
|
||||
const diffMs: number = expiresDate.getTime() - now.getTime();
|
||||
const diffDays: number = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
return CompareCriteria.compareCriteriaNumbers({
|
||||
value: diffDays,
|
||||
threshold: threshold as number,
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
}
|
||||
|
||||
// Check domain registrar
|
||||
if (input.criteriaFilter.checkOn === CheckOn.DomainRegistrar) {
|
||||
if (!domainResponse?.registrar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CompareCriteria.compareCriteriaStrings({
|
||||
value: domainResponse.registrar,
|
||||
threshold: String(threshold),
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
}
|
||||
|
||||
// Check domain name server
|
||||
if (input.criteriaFilter.checkOn === CheckOn.DomainNameServer) {
|
||||
if (
|
||||
!domainResponse?.nameServers ||
|
||||
domainResponse.nameServers.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if any name server matches the criteria
|
||||
for (const nameServer of domainResponse.nameServers) {
|
||||
const result: string | null = CompareCriteria.compareCriteriaStrings({
|
||||
value: nameServer,
|
||||
threshold: String(threshold),
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return `Domain name server: ${result}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check domain status code
|
||||
if (input.criteriaFilter.checkOn === CheckOn.DomainStatusCode) {
|
||||
if (
|
||||
!domainResponse?.domainStatus ||
|
||||
domainResponse.domainStatus.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if any status matches the criteria
|
||||
for (const status of domainResponse.domainStatus) {
|
||||
const result: string | null = CompareCriteria.compareCriteriaStrings({
|
||||
value: status,
|
||||
threshold: String(threshold),
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return `Domain status: ${result}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if domain is expired
|
||||
if (input.criteriaFilter.checkOn === CheckOn.DomainIsExpired) {
|
||||
const isTrue: boolean =
|
||||
input.criteriaFilter.filterType === FilterType.True;
|
||||
const isFalse: boolean =
|
||||
input.criteriaFilter.filterType === FilterType.False;
|
||||
|
||||
if (!domainResponse?.expiresDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresDate: Date = new Date(domainResponse.expiresDate);
|
||||
const now: Date = new Date();
|
||||
const isExpired: boolean = expiresDate.getTime() < now.getTime();
|
||||
|
||||
if (isExpired && isTrue) {
|
||||
return `Domain is expired (expired on ${domainResponse.expiresDate}).`;
|
||||
}
|
||||
|
||||
if (!isExpired && isFalse) {
|
||||
return `Domain is not expired (expires on ${domainResponse.expiresDate}).`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import TraceMonitorCriteria from "./Criteria/TraceMonitorCriteria";
|
||||
import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
|
||||
import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
|
||||
import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
|
||||
import DomainMonitorCriteria from "./Criteria/DomainMonitorCriteria";
|
||||
import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
|
||||
import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
|
||||
import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
|
||||
@@ -506,6 +507,18 @@ ${contextBlock}
|
||||
}
|
||||
}
|
||||
|
||||
if (input.monitor.monitorType === MonitorType.Domain) {
|
||||
const domainMonitorResult: string | null =
|
||||
await DomainMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
||||
dataToProcess: input.dataToProcess,
|
||||
criteriaFilter: input.criteriaFilter,
|
||||
});
|
||||
|
||||
if (domainMonitorResult) {
|
||||
return domainMonitorResult;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import SnmpMonitorResponse, {
|
||||
import DnsMonitorResponse, {
|
||||
DnsRecordResponse,
|
||||
} from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
|
||||
import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
|
||||
import Typeof from "../../../Types/Typeof";
|
||||
import VMUtil from "../VM/VMAPI";
|
||||
import DataToProcess from "./DataToProcess";
|
||||
@@ -277,6 +278,26 @@ export default class MonitorTemplateUtil {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.monitorType === MonitorType.Domain) {
|
||||
const domainResponse: DomainMonitorResponse | undefined = (
|
||||
data.dataToProcess as ProbeMonitorResponse
|
||||
).domainResponse;
|
||||
|
||||
storageMap = {
|
||||
isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline,
|
||||
responseTimeInMs: domainResponse?.responseTimeInMs,
|
||||
failureCause: domainResponse?.failureCause,
|
||||
domainName: domainResponse?.domainName,
|
||||
registrar: domainResponse?.registrar,
|
||||
createdDate: domainResponse?.createdDate,
|
||||
updatedDate: domainResponse?.updatedDate,
|
||||
expiresDate: domainResponse?.expiresDate,
|
||||
nameServers: domainResponse?.nameServers,
|
||||
domainStatus: domainResponse?.domainStatus,
|
||||
dnssec: domainResponse?.dnssec,
|
||||
} as JSONObject;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,13 @@ export enum CheckOn {
|
||||
DnsRecordValue = "DNS Record Value",
|
||||
DnssecIsValid = "DNSSEC Is Valid",
|
||||
DnsRecordExists = "DNS Record Exists",
|
||||
|
||||
// Domain monitors.
|
||||
DomainExpiresDaysIn = "Domain Expires In Days",
|
||||
DomainRegistrar = "Domain Registrar",
|
||||
DomainNameServer = "Domain Name Server",
|
||||
DomainStatusCode = "Domain Status Code",
|
||||
DomainIsExpired = "Domain Is Expired",
|
||||
}
|
||||
|
||||
export interface ServerMonitorOptions {
|
||||
@@ -151,7 +158,8 @@ export class CriteriaFilterUtil {
|
||||
if (
|
||||
checkOn === CheckOn.IsOnline ||
|
||||
checkOn === CheckOn.SnmpIsOnline ||
|
||||
checkOn === CheckOn.DnsIsOnline
|
||||
checkOn === CheckOn.DnsIsOnline ||
|
||||
checkOn === CheckOn.DomainIsExpired
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
15
Common/Types/Monitor/DomainMonitor/DomainMonitorResponse.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export default interface DomainMonitorResponse {
|
||||
isOnline: boolean;
|
||||
responseTimeInMs: number;
|
||||
failureCause: string;
|
||||
domainName: string;
|
||||
registrar?: string | undefined;
|
||||
registrarUrl?: string | undefined;
|
||||
createdDate?: string | undefined;
|
||||
updatedDate?: string | undefined;
|
||||
expiresDate?: string | undefined;
|
||||
nameServers?: Array<string> | undefined;
|
||||
dnssec?: string | undefined;
|
||||
domainStatus?: Array<string> | undefined;
|
||||
isTimeout?: boolean | undefined;
|
||||
}
|
||||
@@ -421,6 +421,33 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
return monitorCriteriaInstance;
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Domain) {
|
||||
const monitorCriteriaInstance: MonitorCriteriaInstance =
|
||||
new MonitorCriteriaInstance();
|
||||
|
||||
monitorCriteriaInstance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
monitorStatusId: arg.monitorStatusId,
|
||||
filterCondition: FilterCondition.All,
|
||||
filters: [
|
||||
{
|
||||
checkOn: CheckOn.DomainIsExpired,
|
||||
filterType: FilterType.False,
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
incidents: [],
|
||||
alerts: [],
|
||||
createAlerts: false,
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: false,
|
||||
name: `Check if ${arg.monitorName} is not expired`,
|
||||
description: `This criteria checks if the ${arg.monitorName} domain registration is not expired`,
|
||||
};
|
||||
|
||||
return monitorCriteriaInstance;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -562,6 +589,46 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
|
||||
};
|
||||
}
|
||||
|
||||
if (arg.monitorType === MonitorType.Domain) {
|
||||
monitorCriteriaInstance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
monitorStatusId: arg.monitorStatusId,
|
||||
filterCondition: FilterCondition.Any,
|
||||
filters: [
|
||||
{
|
||||
checkOn: CheckOn.DomainIsExpired,
|
||||
filterType: FilterType.True,
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${arg.monitorName} domain is expired`,
|
||||
description: `${arg.monitorName} domain registration has expired.`,
|
||||
incidentSeverityId: arg.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
createAlerts: false,
|
||||
alerts: [
|
||||
{
|
||||
title: `${arg.monitorName} domain is expired`,
|
||||
description: `${arg.monitorName} domain registration has expired.`,
|
||||
alertSeverityId: arg.alertSeverityId,
|
||||
autoResolveAlert: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
onCallPolicyIds: [],
|
||||
},
|
||||
],
|
||||
name: `Check if ${arg.monitorName} domain is expired`,
|
||||
description: `This criteria checks if the ${arg.monitorName} domain registration has expired`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
arg.monitorType === MonitorType.API ||
|
||||
arg.monitorType === MonitorType.Website
|
||||
|
||||
@@ -32,6 +32,9 @@ import MonitorStepSnmpMonitor, {
|
||||
import MonitorStepDnsMonitor, {
|
||||
MonitorStepDnsMonitorUtil,
|
||||
} from "./MonitorStepDnsMonitor";
|
||||
import MonitorStepDomainMonitor, {
|
||||
MonitorStepDomainMonitorUtil,
|
||||
} from "./MonitorStepDomainMonitor";
|
||||
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
|
||||
|
||||
export interface MonitorStepType {
|
||||
@@ -78,6 +81,9 @@ export interface MonitorStepType {
|
||||
|
||||
// DNS monitor
|
||||
dnsMonitor?: MonitorStepDnsMonitor | undefined;
|
||||
|
||||
// Domain monitor
|
||||
domainMonitor?: MonitorStepDomainMonitor | undefined;
|
||||
}
|
||||
|
||||
export default class MonitorStep extends DatabaseProperty {
|
||||
@@ -105,6 +111,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
exceptionMonitor: undefined,
|
||||
snmpMonitor: undefined,
|
||||
dnsMonitor: undefined,
|
||||
domainMonitor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,6 +144,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
exceptionMonitor: undefined,
|
||||
snmpMonitor: undefined,
|
||||
dnsMonitor: undefined,
|
||||
domainMonitor: undefined,
|
||||
};
|
||||
|
||||
return monitorStep;
|
||||
@@ -237,6 +245,13 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
return this;
|
||||
}
|
||||
|
||||
public setDomainMonitor(
|
||||
domainMonitor: MonitorStepDomainMonitor,
|
||||
): MonitorStep {
|
||||
this.data!.domainMonitor = domainMonitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setCustomCode(customCode: string): MonitorStep {
|
||||
this.data!.customCode = customCode;
|
||||
return this;
|
||||
@@ -355,6 +370,16 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
}
|
||||
}
|
||||
|
||||
if (monitorType === MonitorType.Domain) {
|
||||
if (!value.data.domainMonitor) {
|
||||
return "Domain configuration is required";
|
||||
}
|
||||
|
||||
if (!value.data.domainMonitor.domainName) {
|
||||
return "Domain name is required";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -403,6 +428,9 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
dnsMonitor: this.data.dnsMonitor
|
||||
? MonitorStepDnsMonitorUtil.toJSON(this.data.dnsMonitor)
|
||||
: undefined,
|
||||
domainMonitor: this.data.domainMonitor
|
||||
? MonitorStepDomainMonitorUtil.toJSON(this.data.domainMonitor)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -511,6 +539,9 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
dnsMonitor: json["dnsMonitor"]
|
||||
? (json["dnsMonitor"] as JSONObject)
|
||||
: undefined,
|
||||
domainMonitor: json["domainMonitor"]
|
||||
? (json["domainMonitor"] as JSONObject)
|
||||
: undefined,
|
||||
}) as any;
|
||||
|
||||
return monitorStep;
|
||||
@@ -537,6 +568,7 @@ export default class MonitorStep extends DatabaseProperty {
|
||||
metricMonitor: Zod.any().optional(),
|
||||
snmpMonitor: Zod.any().optional(),
|
||||
dnsMonitor: Zod.any().optional(),
|
||||
domainMonitor: Zod.any().optional(),
|
||||
}).openapi({
|
||||
type: "object",
|
||||
example: {
|
||||
|
||||
33
Common/Types/Monitor/MonitorStepDomainMonitor.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { JSONObject } from "../JSON";
|
||||
|
||||
export default interface MonitorStepDomainMonitor {
|
||||
domainName: string;
|
||||
timeout: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export class MonitorStepDomainMonitorUtil {
|
||||
public static getDefault(): MonitorStepDomainMonitor {
|
||||
return {
|
||||
domainName: "",
|
||||
timeout: 10000,
|
||||
retries: 3,
|
||||
};
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): MonitorStepDomainMonitor {
|
||||
return {
|
||||
domainName: (json["domainName"] as string) || "",
|
||||
timeout: (json["timeout"] as number) || 10000,
|
||||
retries: (json["retries"] as number) || 3,
|
||||
};
|
||||
}
|
||||
|
||||
public static toJSON(monitor: MonitorStepDomainMonitor): JSONObject {
|
||||
return {
|
||||
domainName: monitor.domainName,
|
||||
timeout: monitor.timeout,
|
||||
retries: monitor.retries,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ enum MonitorType {
|
||||
|
||||
// DNS monitoring
|
||||
DNS = "DNS",
|
||||
|
||||
// Domain registration monitoring
|
||||
Domain = "Domain",
|
||||
}
|
||||
|
||||
export default MonitorType;
|
||||
@@ -40,7 +43,58 @@ export interface MonitorTypeProps {
|
||||
icon: IconProp;
|
||||
}
|
||||
|
||||
export interface MonitorTypeCategory {
|
||||
label: string;
|
||||
monitorTypes: Array<MonitorType>;
|
||||
}
|
||||
|
||||
export class MonitorTypeHelper {
|
||||
public static getMonitorTypeCategories(): Array<MonitorTypeCategory> {
|
||||
return [
|
||||
{
|
||||
label: "Basic Monitoring",
|
||||
monitorTypes: [
|
||||
MonitorType.Website,
|
||||
MonitorType.API,
|
||||
MonitorType.Ping,
|
||||
MonitorType.IP,
|
||||
MonitorType.Port,
|
||||
MonitorType.DNS,
|
||||
MonitorType.SSLCertificate,
|
||||
MonitorType.Domain,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Synthetic Monitoring",
|
||||
monitorTypes: [
|
||||
MonitorType.SyntheticMonitor,
|
||||
MonitorType.CustomJavaScriptCode,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Inbound Monitoring",
|
||||
monitorTypes: [MonitorType.IncomingRequest, MonitorType.IncomingEmail],
|
||||
},
|
||||
{
|
||||
label: "Infrastructure",
|
||||
monitorTypes: [MonitorType.Server, MonitorType.SNMP],
|
||||
},
|
||||
{
|
||||
label: "Telemetry",
|
||||
monitorTypes: [
|
||||
MonitorType.Logs,
|
||||
MonitorType.Metrics,
|
||||
MonitorType.Traces,
|
||||
MonitorType.Exceptions,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Other",
|
||||
monitorTypes: [MonitorType.Manual],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public static isTelemetryMonitor(monitorType: MonitorType): boolean {
|
||||
return (
|
||||
monitorType === MonitorType.Logs ||
|
||||
@@ -189,6 +243,13 @@ export class MonitorTypeHelper {
|
||||
"This monitor type lets you monitor DNS resolution for your domains, verify record values, and check DNSSEC validity.",
|
||||
icon: IconProp.GlobeAlt,
|
||||
},
|
||||
{
|
||||
monitorType: MonitorType.Domain,
|
||||
title: "Domain",
|
||||
description:
|
||||
"This monitor type lets you monitor domain registration health — expiry dates, registrar info, nameserver delegation, and WHOIS status.",
|
||||
icon: IconProp.Globe,
|
||||
},
|
||||
];
|
||||
|
||||
return monitorTypeProps;
|
||||
@@ -235,7 +296,8 @@ export class MonitorTypeHelper {
|
||||
monitorType === MonitorType.SyntheticMonitor ||
|
||||
monitorType === MonitorType.CustomJavaScriptCode ||
|
||||
monitorType === MonitorType.SNMP ||
|
||||
monitorType === MonitorType.DNS;
|
||||
monitorType === MonitorType.DNS ||
|
||||
monitorType === MonitorType.Domain;
|
||||
return isProbeableMonitor;
|
||||
}
|
||||
|
||||
@@ -258,6 +320,7 @@ export class MonitorTypeHelper {
|
||||
MonitorType.Exceptions,
|
||||
MonitorType.SNMP,
|
||||
MonitorType.DNS,
|
||||
MonitorType.Domain,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -291,7 +354,8 @@ export class MonitorTypeHelper {
|
||||
monitorType === MonitorType.SyntheticMonitor ||
|
||||
monitorType === MonitorType.CustomJavaScriptCode ||
|
||||
monitorType === MonitorType.SNMP ||
|
||||
monitorType === MonitorType.DNS
|
||||
monitorType === MonitorType.DNS ||
|
||||
monitorType === MonitorType.Domain
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import SslMonitorResponse from "../Monitor/SSLMonitor/SslMonitorResponse";
|
||||
import SyntheticMonitorResponse from "../Monitor/SyntheticMonitors/SyntheticMonitorResponse";
|
||||
import SnmpMonitorResponse from "../Monitor/SnmpMonitor/SnmpMonitorResponse";
|
||||
import DnsMonitorResponse from "../Monitor/DnsMonitor/DnsMonitorResponse";
|
||||
import DomainMonitorResponse from "../Monitor/DomainMonitor/DomainMonitorResponse";
|
||||
import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
|
||||
import ObjectID from "../ObjectID";
|
||||
import Port from "../Port";
|
||||
@@ -32,6 +33,7 @@ export default interface ProbeMonitorResponse {
|
||||
customCodeMonitorResponse?: CustomCodeMonitorResponse | undefined;
|
||||
snmpResponse?: SnmpMonitorResponse | undefined;
|
||||
dnsResponse?: DnsMonitorResponse | undefined;
|
||||
domainResponse?: DomainMonitorResponse | undefined;
|
||||
monitoredAt: Date;
|
||||
isTimeout?: boolean | undefined;
|
||||
ingestedAt?: Date | undefined;
|
||||
|
||||
@@ -9,8 +9,22 @@ export interface CardSelectOption {
|
||||
icon: IconProp;
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
export interface CardSelectOptionGroup {
|
||||
label: string;
|
||||
options: Array<CardSelectOption>;
|
||||
}
|
||||
|
||||
export function isCardSelectOptionGroup(
|
||||
option: CardSelectOption | CardSelectOptionGroup,
|
||||
): option is CardSelectOptionGroup {
|
||||
return (
|
||||
(option as CardSelectOptionGroup).label !== undefined &&
|
||||
Array.isArray((option as CardSelectOptionGroup).options)
|
||||
);
|
||||
}
|
||||
|
||||
export interface ComponentProps {
|
||||
options: Array<CardSelectOption | CardSelectOptionGroup>;
|
||||
value?: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
error?: string | undefined;
|
||||
@@ -18,80 +32,132 @@ export interface ComponentProps {
|
||||
dataTestId?: string | undefined;
|
||||
}
|
||||
|
||||
interface RenderGroup {
|
||||
label: string | null;
|
||||
options: Array<CardSelectOption>;
|
||||
}
|
||||
|
||||
const CardSelect: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
// Normalize options into render groups
|
||||
const groups: Array<RenderGroup> = [];
|
||||
let ungroupedOptions: Array<CardSelectOption> = [];
|
||||
|
||||
for (const option of props.options) {
|
||||
if (isCardSelectOptionGroup(option)) {
|
||||
// Flush any accumulated ungrouped options first
|
||||
if (ungroupedOptions.length > 0) {
|
||||
groups.push({ label: null, options: ungroupedOptions });
|
||||
ungroupedOptions = [];
|
||||
}
|
||||
groups.push({ label: option.label, options: option.options });
|
||||
} else {
|
||||
ungroupedOptions.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (ungroupedOptions.length > 0) {
|
||||
groups.push({ label: null, options: ungroupedOptions });
|
||||
}
|
||||
|
||||
let cardIndex: number = 0;
|
||||
|
||||
return (
|
||||
<div data-testid={props.dataTestId}>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Select an option"
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{props.options.map((option: CardSelectOption, index: number) => {
|
||||
const isSelected: boolean = props.value === option.value;
|
||||
|
||||
<div role="radiogroup" aria-label="Select an option">
|
||||
{groups.map((group: RenderGroup, groupIndex: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
tabIndex={props.tabIndex ? props.tabIndex + index : index}
|
||||
onClick={() => {
|
||||
props.onChange(option.value);
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
props.onChange(option.value);
|
||||
}
|
||||
}}
|
||||
className={`relative flex cursor-pointer rounded-lg border p-4 shadow-sm focus:outline-none transition-all duration-200 hover:border-indigo-400 hover:shadow-md ${
|
||||
isSelected
|
||||
? "border-indigo-500 bg-indigo-50/50"
|
||||
: "border-gray-200 bg-white"
|
||||
}`}
|
||||
role="radio"
|
||||
aria-checked={isSelected}
|
||||
data-testid={`card-select-option-${option.value}`}
|
||||
>
|
||||
<div className="flex w-full items-start">
|
||||
<div
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
|
||||
isSelected ? "bg-indigo-100" : "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
size={SizeProp.Large}
|
||||
className={`h-5 w-5 ${
|
||||
isSelected ? "text-indigo-600" : "text-gray-600"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 flex-1">
|
||||
<span
|
||||
className={`block text-sm font-semibold ${
|
||||
isSelected ? "text-gray-900" : "text-gray-900"
|
||||
}`}
|
||||
<div key={groupIndex} className={groupIndex > 0 ? "mt-8" : ""}>
|
||||
{group.label && (
|
||||
<div className="relative mb-4">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{option.title}
|
||||
</span>
|
||||
<span
|
||||
className={`mt-1 block text-sm ${
|
||||
isSelected ? "text-gray-600" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="flex-shrink-0 ml-2">
|
||||
<Icon
|
||||
icon={IconProp.CheckCircle}
|
||||
size={SizeProp.Large}
|
||||
className="h-5 w-5 text-indigo-500"
|
||||
/>
|
||||
<div className="w-full border-t border-gray-200"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex justify-start">
|
||||
<span className="bg-white pr-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
{group.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{group.options.map((option: CardSelectOption) => {
|
||||
const isSelected: boolean = props.value === option.value;
|
||||
const currentIndex: number = cardIndex++;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
tabIndex={
|
||||
props.tabIndex
|
||||
? props.tabIndex + currentIndex
|
||||
: currentIndex
|
||||
}
|
||||
onClick={() => {
|
||||
props.onChange(option.value);
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
props.onChange(option.value);
|
||||
}
|
||||
}}
|
||||
className={`relative flex cursor-pointer rounded-lg border p-4 shadow-sm focus:outline-none transition-all duration-200 hover:border-indigo-400 hover:shadow-md ${
|
||||
isSelected
|
||||
? "border-indigo-500 bg-indigo-50/50"
|
||||
: "border-gray-200 bg-white"
|
||||
}`}
|
||||
role="radio"
|
||||
aria-checked={isSelected}
|
||||
data-testid={`card-select-option-${option.value}`}
|
||||
>
|
||||
<div className="flex w-full items-start">
|
||||
<div
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
|
||||
isSelected ? "bg-indigo-100" : "bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
size={SizeProp.Large}
|
||||
className={`h-5 w-5 ${
|
||||
isSelected ? "text-indigo-600" : "text-gray-600"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 flex-1">
|
||||
<span
|
||||
className={`block text-sm font-semibold ${
|
||||
isSelected ? "text-gray-900" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
{option.title}
|
||||
</span>
|
||||
<span
|
||||
className={`mt-1 block text-sm ${
|
||||
isSelected ? "text-gray-600" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="flex-shrink-0 ml-2">
|
||||
<Icon
|
||||
icon={IconProp.CheckCircle}
|
||||
size={SizeProp.Large}
|
||||
className="h-5 w-5 text-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,10 @@ import {
|
||||
CategoryCheckboxOption,
|
||||
CheckboxCategory,
|
||||
} from "../../CategoryCheckbox/CategoryCheckboxTypes";
|
||||
import { CardSelectOption } from "../../CardSelect/CardSelect";
|
||||
import {
|
||||
CardSelectOption,
|
||||
CardSelectOptionGroup,
|
||||
} from "../../CardSelect/CardSelect";
|
||||
import { DropdownOption, DropdownOptionGroup } from "../../Dropdown/Dropdown";
|
||||
import { RadioButton } from "../../RadioButtons/GroupRadioButtons";
|
||||
import FormFieldSchemaType from "./FormFieldSchemaType";
|
||||
@@ -51,7 +54,9 @@ export default interface Field<TEntity> {
|
||||
stepId?: string | undefined;
|
||||
required?: boolean | ((item: FormValues<TEntity>) => boolean) | undefined;
|
||||
dropdownOptions?: Array<DropdownOption | DropdownOptionGroup> | undefined;
|
||||
cardSelectOptions?: Array<CardSelectOption> | undefined;
|
||||
cardSelectOptions?:
|
||||
| Array<CardSelectOption | CardSelectOptionGroup>
|
||||
| undefined;
|
||||
fetchDropdownOptions?:
|
||||
| ((
|
||||
item: FormValues<TEntity>,
|
||||
|
||||
26
Common/UI/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
115
Common/package-lock.json
generated
@@ -930,54 +930,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/cst-dts-gen": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
||||
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
|
||||
"integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/gast": "11.1.1",
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@chevrotain/gast": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
||||
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
|
||||
"integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/gast/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@chevrotain/regexp-to-ast": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
||||
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
|
||||
"integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@chevrotain/types": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
||||
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
|
||||
"integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@chevrotain/utils": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
|
||||
"integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@clickhouse/client": {
|
||||
@@ -2200,12 +2188,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mermaid-js/parser": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
|
||||
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
|
||||
"integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"langium": "3.3.1"
|
||||
"langium": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
@@ -7073,17 +7061,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chevrotain": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
|
||||
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
"@chevrotain/regexp-to-ast": "11.0.3",
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"@chevrotain/utils": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/cst-dts-gen": "11.1.1",
|
||||
"@chevrotain/gast": "11.1.1",
|
||||
"@chevrotain/regexp-to-ast": "11.1.1",
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"@chevrotain/utils": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"node_modules/chevrotain-allstar": {
|
||||
@@ -7098,12 +7086,6 @@
|
||||
"chevrotain": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chevrotain/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@@ -12177,19 +12159,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/langium": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
|
||||
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz",
|
||||
"integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chevrotain": "~11.0.3",
|
||||
"chevrotain-allstar": "~0.3.0",
|
||||
"chevrotain": "~11.1.1",
|
||||
"chevrotain-allstar": "~0.3.1",
|
||||
"vscode-languageserver": "~9.0.1",
|
||||
"vscode-languageserver-textdocument": "~1.0.11",
|
||||
"vscode-uri": "~3.0.8"
|
||||
"vscode-uri": "~3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=20.10.0",
|
||||
"npm": ">=10.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/layout-base": {
|
||||
@@ -13091,14 +13074,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
|
||||
"integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
|
||||
"version": "11.12.3",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz",
|
||||
"integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@mermaid-js/parser": "^0.6.3",
|
||||
"@mermaid-js/parser": "^1.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
@@ -13110,7 +13093,7 @@
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.17.23",
|
||||
"marked": "^16.2.1",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
@@ -18617,9 +18600,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
|
||||
26
Dashboard/index.d.ts
vendored
@@ -2,3 +2,29 @@ declare module "*.png";
|
||||
declare module "*.svg";
|
||||
declare module "*.jpg";
|
||||
declare module "*.gif";
|
||||
|
||||
declare module "react-syntax-highlighter/dist/esm/prism-light";
|
||||
declare module "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
|
||||
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";
|
||||
|
||||
405
Dashboard/package-lock.json
generated
@@ -386,45 +386,45 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/client-sso": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz",
|
||||
"integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.990.0.tgz",
|
||||
"integrity": "sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/middleware-host-header": "^3.972.3",
|
||||
"@aws-sdk/middleware-logger": "^3.972.3",
|
||||
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.5",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.10",
|
||||
"@aws-sdk/region-config-resolver": "^3.972.3",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@aws-sdk/util-endpoints": "3.980.0",
|
||||
"@aws-sdk/util-endpoints": "3.990.0",
|
||||
"@aws-sdk/util-user-agent-browser": "^3.972.3",
|
||||
"@aws-sdk/util-user-agent-node": "^3.972.3",
|
||||
"@aws-sdk/util-user-agent-node": "^3.972.8",
|
||||
"@smithy/config-resolver": "^4.4.6",
|
||||
"@smithy/core": "^3.22.0",
|
||||
"@smithy/core": "^3.23.0",
|
||||
"@smithy/fetch-http-handler": "^5.3.9",
|
||||
"@smithy/hash-node": "^4.2.8",
|
||||
"@smithy/invalid-dependency": "^4.2.8",
|
||||
"@smithy/middleware-content-length": "^4.2.8",
|
||||
"@smithy/middleware-endpoint": "^4.4.12",
|
||||
"@smithy/middleware-retry": "^4.4.29",
|
||||
"@smithy/middleware-endpoint": "^4.4.14",
|
||||
"@smithy/middleware-retry": "^4.4.31",
|
||||
"@smithy/middleware-serde": "^4.2.9",
|
||||
"@smithy/middleware-stack": "^4.2.8",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/node-http-handler": "^4.4.8",
|
||||
"@smithy/node-http-handler": "^4.4.10",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/smithy-client": "^4.11.1",
|
||||
"@smithy/smithy-client": "^4.11.3",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/url-parser": "^4.2.8",
|
||||
"@smithy/util-base64": "^4.3.0",
|
||||
"@smithy/util-body-length-browser": "^4.2.0",
|
||||
"@smithy/util-body-length-node": "^4.2.1",
|
||||
"@smithy/util-defaults-mode-browser": "^4.3.28",
|
||||
"@smithy/util-defaults-mode-node": "^4.2.31",
|
||||
"@smithy/util-defaults-mode-browser": "^4.3.30",
|
||||
"@smithy/util-defaults-mode-node": "^4.2.33",
|
||||
"@smithy/util-endpoints": "^3.2.8",
|
||||
"@smithy/util-middleware": "^4.2.8",
|
||||
"@smithy/util-retry": "^4.2.8",
|
||||
@@ -436,9 +436,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
|
||||
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
|
||||
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -453,20 +453,20 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/core": {
|
||||
"version": "3.973.5",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
|
||||
"integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
|
||||
"version": "3.973.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.10.tgz",
|
||||
"integrity": "sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@aws-sdk/xml-builder": "^3.972.2",
|
||||
"@smithy/core": "^3.22.0",
|
||||
"@aws-sdk/xml-builder": "^3.972.4",
|
||||
"@smithy/core": "^3.23.0",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/signature-v4": "^5.3.8",
|
||||
"@smithy/smithy-client": "^4.11.1",
|
||||
"@smithy/smithy-client": "^4.11.3",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/util-base64": "^4.3.0",
|
||||
"@smithy/util-middleware": "^4.2.8",
|
||||
@@ -478,13 +478,13 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz",
|
||||
"integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.8.tgz",
|
||||
"integrity": "sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/types": "^4.12.0",
|
||||
@@ -495,21 +495,21 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.5",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz",
|
||||
"integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==",
|
||||
"version": "3.972.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.10.tgz",
|
||||
"integrity": "sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/fetch-http-handler": "^5.3.9",
|
||||
"@smithy/node-http-handler": "^4.4.8",
|
||||
"@smithy/node-http-handler": "^4.4.10",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/smithy-client": "^4.11.1",
|
||||
"@smithy/smithy-client": "^4.11.3",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/util-stream": "^4.5.10",
|
||||
"@smithy/util-stream": "^4.5.12",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -517,20 +517,20 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz",
|
||||
"integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.8.tgz",
|
||||
"integrity": "sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.5",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.3",
|
||||
"@aws-sdk/nested-clients": "3.980.0",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.10",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.8",
|
||||
"@aws-sdk/nested-clients": "3.990.0",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/credential-provider-imds": "^4.2.8",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
@@ -543,14 +543,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
|
||||
"integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.8.tgz",
|
||||
"integrity": "sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/nested-clients": "3.980.0",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/nested-clients": "3.990.0",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
@@ -563,18 +563,18 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.4",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz",
|
||||
"integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==",
|
||||
"version": "3.972.9",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.9.tgz",
|
||||
"integrity": "sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.5",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.3",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.10",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.8",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.8",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/credential-provider-imds": "^4.2.8",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
@@ -587,13 +587,13 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz",
|
||||
"integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.8.tgz",
|
||||
"integrity": "sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/shared-ini-file-loader": "^4.4.3",
|
||||
@@ -605,15 +605,15 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz",
|
||||
"integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.8.tgz",
|
||||
"integrity": "sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sso": "3.980.0",
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/token-providers": "3.980.0",
|
||||
"@aws-sdk/client-sso": "3.990.0",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/token-providers": "3.990.0",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/shared-ini-file-loader": "^4.4.3",
|
||||
@@ -625,14 +625,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz",
|
||||
"integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.8.tgz",
|
||||
"integrity": "sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/nested-clients": "3.980.0",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/nested-clients": "3.990.0",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/shared-ini-file-loader": "^4.4.3",
|
||||
@@ -692,16 +692,16 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/middleware-user-agent": {
|
||||
"version": "3.972.5",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
|
||||
"integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
|
||||
"version": "3.972.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.10.tgz",
|
||||
"integrity": "sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@aws-sdk/util-endpoints": "3.980.0",
|
||||
"@smithy/core": "^3.22.0",
|
||||
"@aws-sdk/util-endpoints": "3.990.0",
|
||||
"@smithy/core": "^3.23.0",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -711,9 +711,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
|
||||
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
|
||||
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -728,45 +728,45 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
|
||||
"integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz",
|
||||
"integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-js": "5.2.0",
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/middleware-host-header": "^3.972.3",
|
||||
"@aws-sdk/middleware-logger": "^3.972.3",
|
||||
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.5",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.10",
|
||||
"@aws-sdk/region-config-resolver": "^3.972.3",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@aws-sdk/util-endpoints": "3.980.0",
|
||||
"@aws-sdk/util-endpoints": "3.990.0",
|
||||
"@aws-sdk/util-user-agent-browser": "^3.972.3",
|
||||
"@aws-sdk/util-user-agent-node": "^3.972.3",
|
||||
"@aws-sdk/util-user-agent-node": "^3.972.8",
|
||||
"@smithy/config-resolver": "^4.4.6",
|
||||
"@smithy/core": "^3.22.0",
|
||||
"@smithy/core": "^3.23.0",
|
||||
"@smithy/fetch-http-handler": "^5.3.9",
|
||||
"@smithy/hash-node": "^4.2.8",
|
||||
"@smithy/invalid-dependency": "^4.2.8",
|
||||
"@smithy/middleware-content-length": "^4.2.8",
|
||||
"@smithy/middleware-endpoint": "^4.4.12",
|
||||
"@smithy/middleware-retry": "^4.4.29",
|
||||
"@smithy/middleware-endpoint": "^4.4.14",
|
||||
"@smithy/middleware-retry": "^4.4.31",
|
||||
"@smithy/middleware-serde": "^4.2.9",
|
||||
"@smithy/middleware-stack": "^4.2.8",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/node-http-handler": "^4.4.8",
|
||||
"@smithy/node-http-handler": "^4.4.10",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/smithy-client": "^4.11.1",
|
||||
"@smithy/smithy-client": "^4.11.3",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/url-parser": "^4.2.8",
|
||||
"@smithy/util-base64": "^4.3.0",
|
||||
"@smithy/util-body-length-browser": "^4.2.0",
|
||||
"@smithy/util-body-length-node": "^4.2.1",
|
||||
"@smithy/util-defaults-mode-browser": "^4.3.28",
|
||||
"@smithy/util-defaults-mode-node": "^4.2.31",
|
||||
"@smithy/util-defaults-mode-browser": "^4.3.30",
|
||||
"@smithy/util-defaults-mode-node": "^4.2.33",
|
||||
"@smithy/util-endpoints": "^3.2.8",
|
||||
"@smithy/util-middleware": "^4.2.8",
|
||||
"@smithy/util-retry": "^4.2.8",
|
||||
@@ -778,9 +778,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
|
||||
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
|
||||
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -812,14 +812,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.980.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz",
|
||||
"integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==",
|
||||
"version": "3.990.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.990.0.tgz",
|
||||
"integrity": "sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.973.5",
|
||||
"@aws-sdk/nested-clients": "3.980.0",
|
||||
"@aws-sdk/core": "^3.973.10",
|
||||
"@aws-sdk/nested-clients": "3.990.0",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/shared-ini-file-loader": "^4.4.3",
|
||||
@@ -888,13 +888,13 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/util-user-agent-node": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
|
||||
"integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
|
||||
"version": "3.972.8",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.8.tgz",
|
||||
"integrity": "sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.5",
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.10",
|
||||
"@aws-sdk/types": "^3.973.1",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/types": "^4.12.0",
|
||||
@@ -913,9 +913,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
|
||||
"integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
|
||||
"version": "3.972.4",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz",
|
||||
"integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1687,54 +1687,42 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/cst-dts-gen": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
||||
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
|
||||
"integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/gast": "11.1.1",
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/gast": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
||||
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
|
||||
"integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/gast/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/regexp-to-ast": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
||||
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
|
||||
"integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/types": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
||||
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
|
||||
"integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"../Common/node_modules/@chevrotain/utils": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
|
||||
"integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"../Common/node_modules/@clickhouse/client": {
|
||||
@@ -3039,12 +3027,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/@mermaid-js/parser": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
|
||||
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
|
||||
"integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"langium": "3.3.1"
|
||||
"langium": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@monaco-editor/loader": {
|
||||
@@ -5084,9 +5072,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/core": {
|
||||
"version": "3.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
|
||||
"integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.2.tgz",
|
||||
"integrity": "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -5096,7 +5084,7 @@
|
||||
"@smithy/util-base64": "^4.3.0",
|
||||
"@smithy/util-body-length-browser": "^4.2.0",
|
||||
"@smithy/util-middleware": "^4.2.8",
|
||||
"@smithy/util-stream": "^4.5.11",
|
||||
"@smithy/util-stream": "^4.5.12",
|
||||
"@smithy/util-utf8": "^4.2.0",
|
||||
"@smithy/uuid": "^1.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -5198,13 +5186,13 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/middleware-endpoint": {
|
||||
"version": "4.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz",
|
||||
"integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==",
|
||||
"version": "4.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.16.tgz",
|
||||
"integrity": "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.22.1",
|
||||
"@smithy/core": "^3.23.2",
|
||||
"@smithy/middleware-serde": "^4.2.9",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/shared-ini-file-loader": "^4.4.3",
|
||||
@@ -5218,16 +5206,16 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/middleware-retry": {
|
||||
"version": "4.4.30",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz",
|
||||
"integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==",
|
||||
"version": "4.4.33",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.33.tgz",
|
||||
"integrity": "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/service-error-classification": "^4.2.8",
|
||||
"@smithy/smithy-client": "^4.11.2",
|
||||
"@smithy/smithy-client": "^4.11.5",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/util-middleware": "^4.2.8",
|
||||
"@smithy/util-retry": "^4.2.8",
|
||||
@@ -5284,9 +5272,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
|
||||
"integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz",
|
||||
"integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -5405,18 +5393,18 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/smithy-client": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz",
|
||||
"integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==",
|
||||
"version": "4.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.5.tgz",
|
||||
"integrity": "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.22.1",
|
||||
"@smithy/middleware-endpoint": "^4.4.13",
|
||||
"@smithy/core": "^3.23.2",
|
||||
"@smithy/middleware-endpoint": "^4.4.16",
|
||||
"@smithy/middleware-stack": "^4.2.8",
|
||||
"@smithy/protocol-http": "^5.3.8",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/util-stream": "^4.5.11",
|
||||
"@smithy/util-stream": "^4.5.12",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5520,14 +5508,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/util-defaults-mode-browser": {
|
||||
"version": "4.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz",
|
||||
"integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==",
|
||||
"version": "4.3.32",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.32.tgz",
|
||||
"integrity": "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/smithy-client": "^4.11.2",
|
||||
"@smithy/smithy-client": "^4.11.5",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -5536,9 +5524,9 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/util-defaults-mode-node": {
|
||||
"version": "4.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz",
|
||||
"integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==",
|
||||
"version": "4.2.35",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.35.tgz",
|
||||
"integrity": "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -5546,7 +5534,7 @@
|
||||
"@smithy/credential-provider-imds": "^4.2.8",
|
||||
"@smithy/node-config-provider": "^4.3.8",
|
||||
"@smithy/property-provider": "^4.2.8",
|
||||
"@smithy/smithy-client": "^4.11.2",
|
||||
"@smithy/smithy-client": "^4.11.5",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
@@ -5612,14 +5600,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/@smithy/util-stream": {
|
||||
"version": "4.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz",
|
||||
"integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==",
|
||||
"version": "4.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz",
|
||||
"integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/fetch-http-handler": "^5.3.9",
|
||||
"@smithy/node-http-handler": "^4.4.9",
|
||||
"@smithy/node-http-handler": "^4.4.10",
|
||||
"@smithy/types": "^4.12.0",
|
||||
"@smithy/util-base64": "^4.3.0",
|
||||
"@smithy/util-buffer-from": "^4.2.0",
|
||||
@@ -8040,17 +8028,17 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/chevrotain": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
|
||||
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
"@chevrotain/regexp-to-ast": "11.0.3",
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"@chevrotain/utils": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
"@chevrotain/cst-dts-gen": "11.1.1",
|
||||
"@chevrotain/gast": "11.1.1",
|
||||
"@chevrotain/regexp-to-ast": "11.1.1",
|
||||
"@chevrotain/types": "11.1.1",
|
||||
"@chevrotain/utils": "11.1.1",
|
||||
"lodash-es": "4.17.23"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/chevrotain-allstar": {
|
||||
@@ -8065,12 +8053,6 @@
|
||||
"chevrotain": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/chevrotain/node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
@@ -13296,19 +13278,20 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/langium": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
|
||||
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz",
|
||||
"integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chevrotain": "~11.0.3",
|
||||
"chevrotain-allstar": "~0.3.0",
|
||||
"chevrotain": "~11.1.1",
|
||||
"chevrotain-allstar": "~0.3.1",
|
||||
"vscode-languageserver": "~9.0.1",
|
||||
"vscode-languageserver-textdocument": "~1.0.11",
|
||||
"vscode-uri": "~3.0.8"
|
||||
"vscode-uri": "~3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=20.10.0",
|
||||
"npm": ">=10.2.3"
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/layout-base": {
|
||||
@@ -14106,14 +14089,14 @@
|
||||
}
|
||||
},
|
||||
"../Common/node_modules/mermaid": {
|
||||
"version": "11.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
|
||||
"integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
|
||||
"version": "11.12.3",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz",
|
||||
"integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@mermaid-js/parser": "^0.6.3",
|
||||
"@mermaid-js/parser": "^1.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
@@ -14125,7 +14108,7 @@
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.17.23",
|
||||
"marked": "^16.2.1",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
@@ -19836,9 +19819,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../Common/node_modules/w3c-xmlserializer": {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import MonitorStepDomainMonitor from "Common/Types/Monitor/MonitorStepDomainMonitor";
|
||||
import Input, { InputType } from "Common/UI/Components/Input/Input";
|
||||
import FieldLabelElement from "Common/UI/Components/Forms/Fields/FieldLabel";
|
||||
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStepDomainMonitor: MonitorStepDomainMonitor;
|
||||
onChange: (value: MonitorStepDomainMonitor) => void;
|
||||
}
|
||||
|
||||
const DomainMonitorStepForm: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] =
|
||||
useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<FieldLabelElement
|
||||
title="Domain Name"
|
||||
description="The domain name to monitor (e.g. example.com)"
|
||||
required={true}
|
||||
/>
|
||||
<Input
|
||||
initialValue={props.monitorStepDomainMonitor.domainName}
|
||||
placeholder="example.com"
|
||||
onChange={(value: string) => {
|
||||
props.onChange({
|
||||
...props.monitorStepDomainMonitor,
|
||||
domainName: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!showAdvancedOptions && (
|
||||
<div className="mt-1 -ml-3">
|
||||
<Button
|
||||
title="Advanced: Timeout and Retries"
|
||||
buttonStyle={ButtonStyleType.SECONDARY_LINK}
|
||||
onClick={() => {
|
||||
setShowAdvancedOptions(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAdvancedOptions && (
|
||||
<div className="space-y-4 border p-4 rounded-md bg-gray-50">
|
||||
<h4 className="font-medium">Advanced Options</h4>
|
||||
|
||||
<div>
|
||||
<FieldLabelElement
|
||||
title="Timeout (ms)"
|
||||
description="How long to wait for a WHOIS response before timing out"
|
||||
required={false}
|
||||
/>
|
||||
<Input
|
||||
initialValue={
|
||||
props.monitorStepDomainMonitor.timeout?.toString() || "10000"
|
||||
}
|
||||
placeholder="10000"
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onChange({
|
||||
...props.monitorStepDomainMonitor,
|
||||
timeout: parseInt(value) || 10000,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabelElement
|
||||
title="Retries"
|
||||
description="Number of times to retry on failure"
|
||||
required={false}
|
||||
/>
|
||||
<Input
|
||||
initialValue={
|
||||
props.monitorStepDomainMonitor.retries?.toString() || "3"
|
||||
}
|
||||
placeholder="3"
|
||||
type={InputType.NUMBER}
|
||||
onChange={(value: string) => {
|
||||
props.onChange({
|
||||
...props.monitorStepDomainMonitor,
|
||||
retries: parseInt(value) || 3,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainMonitorStepForm;
|
||||
@@ -79,6 +79,10 @@ import DnsMonitorStepForm from "./DnsMonitor/DnsMonitorStepForm";
|
||||
import MonitorStepDnsMonitor, {
|
||||
MonitorStepDnsMonitorUtil,
|
||||
} from "Common/Types/Monitor/MonitorStepDnsMonitor";
|
||||
import DomainMonitorStepForm from "./DomainMonitor/DomainMonitorStepForm";
|
||||
import MonitorStepDomainMonitor, {
|
||||
MonitorStepDomainMonitorUtil,
|
||||
} from "Common/Types/Monitor/MonitorStepDomainMonitor";
|
||||
|
||||
export interface ComponentProps {
|
||||
monitorStatusDropdownOptions: Array<DropdownOption>;
|
||||
@@ -812,6 +816,24 @@ return {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{props.monitorType === MonitorType.Domain && (
|
||||
<Card
|
||||
title="Domain Monitor Configuration"
|
||||
description="Configure the domain registration monitoring settings"
|
||||
>
|
||||
<DomainMonitorStepForm
|
||||
monitorStepDomainMonitor={
|
||||
monitorStep.data?.domainMonitor ||
|
||||
MonitorStepDomainMonitorUtil.getDefault()
|
||||
}
|
||||
onChange={(value: MonitorStepDomainMonitor) => {
|
||||
monitorStep.setDomainMonitor(value);
|
||||
props.onChange?.(MonitorStep.clone(monitorStep));
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Code Monitor Section */}
|
||||
{isCodeMonitor && (
|
||||
<Card
|
||||
|
||||
@@ -310,6 +310,20 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
|
||||
placeholder: "0",
|
||||
},
|
||||
];
|
||||
} else if (props.monitorType === MonitorType.Domain) {
|
||||
fields = [
|
||||
{
|
||||
key: "domainMonitor",
|
||||
title: "Domain Name",
|
||||
description: "The domain name being monitored via WHOIS.",
|
||||
fieldType: FieldType.Element,
|
||||
placeholder: "No data entered",
|
||||
getElement: (item: MonitorStepType): ReactElement => {
|
||||
const domainMonitor: any = item.domainMonitor;
|
||||
return <p>{domainMonitor?.domainName || "-"}</p>;
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (props.monitorType === MonitorType.Logs) {
|
||||
logFields = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
|
||||
import DomainMonitorResponse from "Common/Types/Monitor/DomainMonitor/DomainMonitorResponse";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
probeMonitorResponse: ProbeMonitorResponse;
|
||||
probeName?: string | undefined;
|
||||
}
|
||||
|
||||
const DomainMonitorView: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const domainResponse: DomainMonitorResponse | undefined =
|
||||
props.probeMonitorResponse?.domainResponse;
|
||||
|
||||
let responseTimeInMs: number = domainResponse?.responseTimeInMs || 0;
|
||||
|
||||
if (responseTimeInMs > 0) {
|
||||
responseTimeInMs = Math.round(responseTimeInMs);
|
||||
}
|
||||
|
||||
type FormatDateText = (dateStr: string | undefined) => string;
|
||||
|
||||
const formatDateText: FormatDateText = (
|
||||
dateStr: string | undefined,
|
||||
): string => {
|
||||
if (!dateStr) {
|
||||
return "-";
|
||||
}
|
||||
try {
|
||||
const date: Date = new Date(dateStr);
|
||||
return OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(date);
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-1/5 shadow-none border-2 border-gray-100"
|
||||
title="Probe"
|
||||
value={props.probeName || "-"}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/5 shadow-none border-2 border-gray-100"
|
||||
title="Status"
|
||||
value={props.probeMonitorResponse.isOnline ? "Online" : "Offline"}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/5 shadow-none border-2 border-gray-100"
|
||||
title="Response Time"
|
||||
value={responseTimeInMs ? responseTimeInMs + " ms" : "-"}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/5 shadow-none border-2 border-gray-100"
|
||||
title="Expires At"
|
||||
value={formatDateText(domainResponse?.expiresDate)}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/5 shadow-none border-2 border-gray-100"
|
||||
title="Monitored At"
|
||||
value={
|
||||
props.probeMonitorResponse?.monitoredAt
|
||||
? OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
||||
props.probeMonitorResponse.monitoredAt,
|
||||
)
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-1/3 shadow-none border-2 border-gray-100"
|
||||
title="Registrar"
|
||||
value={domainResponse?.registrar || "-"}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/3 shadow-none border-2 border-gray-100"
|
||||
title="Created"
|
||||
value={formatDateText(domainResponse?.createdDate)}
|
||||
/>
|
||||
<InfoCard
|
||||
className="w-1/3 shadow-none border-2 border-gray-100"
|
||||
title="DNSSEC"
|
||||
value={domainResponse?.dnssec || "-"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{props.probeMonitorResponse.failureCause && (
|
||||
<div className="flex space-x-3">
|
||||
<InfoCard
|
||||
className="w-full shadow-none border-2 border-gray-100"
|
||||
title="Error"
|
||||
value={props.probeMonitorResponse.failureCause?.toString() || "-"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name Servers Section */}
|
||||
{domainResponse?.nameServers && domainResponse.nameServers.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-700">Name Servers</h3>
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name Server
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{domainResponse.nameServers.map((ns: string, index: number) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 text-sm text-gray-900 font-mono">
|
||||
{ns}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Domain Status Section */}
|
||||
{domainResponse?.domainStatus &&
|
||||
domainResponse.domainStatus.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-gray-700">
|
||||
Domain Status Codes
|
||||
</h3>
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{domainResponse.domainStatus.map(
|
||||
(status: string, index: number) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 text-sm text-gray-900 font-mono">
|
||||
{status}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainMonitorView;
|
||||
@@ -396,7 +396,7 @@ const EvaluationLogList: FunctionComponent<ComponentProps> = (
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="text-base font-semibold text-gray-900">
|
||||
{getSummaryTitle}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import SyntheticMonitorView from "./SyntheticMonitorView";
|
||||
import WebsiteMonitorSummaryView from "./WebsiteMonitorView";
|
||||
import SnmpMonitorView from "./SnmpMonitorView";
|
||||
import DnsMonitorView from "./DnsMonitorView";
|
||||
import DomainMonitorView from "./DomainMonitorView";
|
||||
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
||||
import IncomingEmailMonitorRequest from "Common/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest";
|
||||
import MonitorType, {
|
||||
@@ -131,6 +132,15 @@ const SummaryInfo: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (props.monitorType === MonitorType.Domain) {
|
||||
summaryComponent = (
|
||||
<DomainMonitorView
|
||||
probeMonitorResponse={probeMonitorResponse}
|
||||
probeName={props.probeName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} className="space-y-6">
|
||||
{summaryComponent}
|
||||
|
||||
@@ -72,7 +72,7 @@ const MonitorCreate: FunctionComponent<
|
||||
fieldType: FormFieldSchemaType.CardSelect,
|
||||
required: true,
|
||||
cardSelectOptions:
|
||||
MonitorTypeUtil.monitorTypesAsCardSelectOptions(),
|
||||
MonitorTypeUtil.monitorTypesAsCategorizedCardSelectOptions(),
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import MonitorTable from "../../Components/Monitor/MonitorTable";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import MonitorTable from "../../Components/Monitor/MonitorTable";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const NotOperationalMonitors: FunctionComponent = (): ReactElement => {
|
||||
const NotOperationalMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import MonitorTable from "../../Components/Monitor/MonitorTable";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import MonitorTable from "../../Components/Monitor/MonitorTable";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import IncomingCallNumber from "../../Components/NotificationMethods/IncomingCallNumber";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const IncomingCallPhoneNumbers: FunctionComponent = (): ReactElement => {
|
||||
const IncomingCallPhoneNumbers: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return <IncomingCallNumber />;
|
||||
};
|
||||
|
||||
|
||||
@@ -295,6 +295,18 @@ export default class CriteriaFilterUtil {
|
||||
});
|
||||
}
|
||||
|
||||
if (monitorType === MonitorType.Domain) {
|
||||
options = options.filter((i: DropdownOption) => {
|
||||
return (
|
||||
i.value === CheckOn.DomainExpiresDaysIn ||
|
||||
i.value === CheckOn.DomainRegistrar ||
|
||||
i.value === CheckOn.DomainNameServer ||
|
||||
i.value === CheckOn.DomainStatusCode ||
|
||||
i.value === CheckOn.DomainIsExpired
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -570,6 +582,40 @@ export default class CriteriaFilterUtil {
|
||||
});
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainExpiresDaysIn) {
|
||||
options = options.filter((i: DropdownOption) => {
|
||||
return (
|
||||
i.value === FilterType.GreaterThan ||
|
||||
i.value === FilterType.LessThan ||
|
||||
i.value === FilterType.LessThanOrEqualTo ||
|
||||
i.value === FilterType.GreaterThanOrEqualTo
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
checkOn === CheckOn.DomainRegistrar ||
|
||||
checkOn === CheckOn.DomainNameServer ||
|
||||
checkOn === CheckOn.DomainStatusCode
|
||||
) {
|
||||
options = options.filter((i: DropdownOption) => {
|
||||
return (
|
||||
i.value === FilterType.Contains ||
|
||||
i.value === FilterType.NotContains ||
|
||||
i.value === FilterType.EqualTo ||
|
||||
i.value === FilterType.NotEqualTo ||
|
||||
i.value === FilterType.StartsWith ||
|
||||
i.value === FilterType.EndsWith
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainIsExpired) {
|
||||
options = options.filter((i: DropdownOption) => {
|
||||
return i.value === FilterType.True || i.value === FilterType.False;
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -709,6 +755,22 @@ export default class CriteriaFilterUtil {
|
||||
return "192.168.1.1";
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainExpiresDaysIn) {
|
||||
return "30";
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainRegistrar) {
|
||||
return "GoDaddy";
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainNameServer) {
|
||||
return "ns1.example.com";
|
||||
}
|
||||
|
||||
if (checkOn === CheckOn.DomainStatusCode) {
|
||||
return "clientTransferProhibited";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import {
|
||||
import MonitorType, {
|
||||
MonitorTypeCategory,
|
||||
MonitorTypeHelper,
|
||||
MonitorTypeProps,
|
||||
} from "Common/Types/Monitor/MonitorType";
|
||||
import { CardSelectOption } from "Common/UI/Components/CardSelect/CardSelect";
|
||||
import {
|
||||
CardSelectOption,
|
||||
CardSelectOptionGroup,
|
||||
} from "Common/UI/Components/CardSelect/CardSelect";
|
||||
import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown";
|
||||
|
||||
export default class MonitorTypeUtil {
|
||||
@@ -31,4 +35,43 @@ export default class MonitorTypeUtil {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static monitorTypesAsCategorizedCardSelectOptions(): Array<CardSelectOptionGroup> {
|
||||
const categories: Array<MonitorTypeCategory> =
|
||||
MonitorTypeHelper.getMonitorTypeCategories();
|
||||
const allProps: Array<MonitorTypeProps> =
|
||||
MonitorTypeHelper.getAllMonitorTypeProps();
|
||||
|
||||
return categories.map(
|
||||
(category: MonitorTypeCategory): CardSelectOptionGroup => {
|
||||
return {
|
||||
label: category.label,
|
||||
options: category.monitorTypes
|
||||
.map((monitorType: MonitorType): CardSelectOption | null => {
|
||||
const typeProps: MonitorTypeProps | undefined = allProps.find(
|
||||
(p: MonitorTypeProps) => {
|
||||
return p.monitorType === monitorType;
|
||||
},
|
||||
);
|
||||
|
||||
if (!typeProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: typeProps.monitorType,
|
||||
title: typeProps.title,
|
||||
description: typeProps.description,
|
||||
icon: typeProps.icon,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(option: CardSelectOption | null): option is CardSelectOption => {
|
||||
return option !== null;
|
||||
},
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
155
Docs/Content/cli/authentication.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Authentication
|
||||
|
||||
The OneUptime CLI supports multiple ways to authenticate with your OneUptime instance. You can use named contexts, environment variables, or pass credentials directly as flags.
|
||||
|
||||
## Login
|
||||
|
||||
Authenticate with your OneUptime instance using an API key:
|
||||
|
||||
```bash
|
||||
oneuptime login <api-key> <instance-url>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<api-key>` | Your OneUptime API key (e.g., `sk-your-api-key`) |
|
||||
| `<instance-url>` | Your OneUptime instance URL (e.g., `https://oneuptime.com`) |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--context-name <name>` | Name for this context (default: `"default"`) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Login with default context
|
||||
oneuptime login sk-abc123 https://oneuptime.com
|
||||
|
||||
# Login with a named context
|
||||
oneuptime login sk-abc123 https://oneuptime.com --context-name production
|
||||
|
||||
# Set up multiple environments
|
||||
oneuptime login sk-prod-key https://oneuptime.com --context-name production
|
||||
oneuptime login sk-staging-key https://staging.oneuptime.com --context-name staging
|
||||
```
|
||||
|
||||
## Contexts
|
||||
|
||||
Contexts allow you to save and switch between multiple OneUptime environments (e.g., production, staging, development).
|
||||
|
||||
### List Contexts
|
||||
|
||||
```bash
|
||||
oneuptime context list
|
||||
```
|
||||
|
||||
Displays all configured contexts. The current context is marked with `*`.
|
||||
|
||||
### Switch Context
|
||||
|
||||
```bash
|
||||
oneuptime context use <name>
|
||||
```
|
||||
|
||||
Switch to a different named context for all subsequent commands.
|
||||
|
||||
```bash
|
||||
# Switch to staging
|
||||
oneuptime context use staging
|
||||
|
||||
# Switch to production
|
||||
oneuptime context use production
|
||||
```
|
||||
|
||||
### View Current Context
|
||||
|
||||
```bash
|
||||
oneuptime context current
|
||||
```
|
||||
|
||||
Displays the currently active context, including the instance URL and a masked API key.
|
||||
|
||||
### Delete a Context
|
||||
|
||||
```bash
|
||||
oneuptime context delete <name>
|
||||
```
|
||||
|
||||
Remove a named context. If the deleted context is the current one, the CLI automatically switches to the first remaining context.
|
||||
|
||||
## Credential Resolution
|
||||
|
||||
Credentials are resolved in the following priority order:
|
||||
|
||||
1. **CLI flags** (`--api-key` and `--url`)
|
||||
2. **Environment variables** (`ONEUPTIME_API_KEY` and `ONEUPTIME_URL`)
|
||||
3. **Named context** (via `--context` flag)
|
||||
4. **Current context** (from saved configuration)
|
||||
|
||||
You can mix sources -- for example, use an environment variable for the API key and a saved context for the URL.
|
||||
|
||||
### Using CLI Flags
|
||||
|
||||
```bash
|
||||
oneuptime --api-key sk-abc123 --url https://oneuptime.com incident list
|
||||
```
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
```bash
|
||||
export ONEUPTIME_API_KEY=sk-abc123
|
||||
export ONEUPTIME_URL=https://oneuptime.com
|
||||
|
||||
oneuptime incident list
|
||||
```
|
||||
|
||||
### Using a Specific Context
|
||||
|
||||
```bash
|
||||
oneuptime --context production incident list
|
||||
```
|
||||
|
||||
## Verify Authentication
|
||||
|
||||
Check your current authentication status:
|
||||
|
||||
```bash
|
||||
oneuptime whoami
|
||||
```
|
||||
|
||||
This displays:
|
||||
- Instance URL
|
||||
- Masked API key
|
||||
- Current context name (only shown if a saved context is active)
|
||||
|
||||
If not authenticated, the command shows a helpful message suggesting you run `oneuptime login`.
|
||||
|
||||
## Configuration File
|
||||
|
||||
Credentials are stored in `~/.oneuptime/config.json` with restricted permissions (`0600`).
|
||||
|
||||
```json
|
||||
{
|
||||
"currentContext": "production",
|
||||
"contexts": {
|
||||
"production": {
|
||||
"name": "production",
|
||||
"apiUrl": "https://oneuptime.com",
|
||||
"apiKey": "sk-..."
|
||||
},
|
||||
"staging": {
|
||||
"name": "staging",
|
||||
"apiUrl": "https://staging.oneuptime.com",
|
||||
"apiKey": "sk-..."
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"output": "table",
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
234
Docs/Content/cli/command-reference.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# Command Reference
|
||||
|
||||
Complete reference for all OneUptime CLI commands.
|
||||
|
||||
## Authentication Commands
|
||||
|
||||
### `oneuptime login`
|
||||
|
||||
Authenticate with a OneUptime instance.
|
||||
|
||||
```bash
|
||||
oneuptime login <api-key> <instance-url> [--context-name <name>]
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<api-key>` | argument | Yes | API key for authentication |
|
||||
| `<instance-url>` | argument | Yes | OneUptime instance URL |
|
||||
| `--context-name` | option | No | Context name (default: `"default"`) |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime context list`
|
||||
|
||||
List all saved contexts.
|
||||
|
||||
```bash
|
||||
oneuptime context list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime context use`
|
||||
|
||||
Switch to a named context.
|
||||
|
||||
```bash
|
||||
oneuptime context use <name>
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<name>` | argument | Yes | Context name to activate |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime context current`
|
||||
|
||||
Display the active context with masked API key.
|
||||
|
||||
```bash
|
||||
oneuptime context current
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime context delete`
|
||||
|
||||
Remove a saved context.
|
||||
|
||||
```bash
|
||||
oneuptime context delete <name>
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<name>` | argument | Yes | Context name to delete |
|
||||
|
||||
---
|
||||
|
||||
## Resource Commands
|
||||
|
||||
All resource commands follow the same pattern. Replace `<resource>` with any supported resource name (e.g., `incident`, `monitor`, `alert`, `status-page`).
|
||||
|
||||
### `oneuptime <resource> list`
|
||||
|
||||
List resources with filtering and pagination.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> list [options]
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `--query <json>` | string | None | Filter criteria as JSON |
|
||||
| `--limit <n>` | number | `10` | Maximum results |
|
||||
| `--skip <n>` | number | `0` | Results to skip |
|
||||
| `--sort <json>` | string | None | Sort order as JSON |
|
||||
| `-o, --output` | string | `table` | Output format |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime <resource> get`
|
||||
|
||||
Get a single resource by ID.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> get <id> [-o <format>]
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<id>` | argument | Yes | Resource ID (UUID) |
|
||||
| `-o, --output` | option | No | Output format |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime <resource> create`
|
||||
|
||||
Create a new resource.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> create [--data <json> | --file <path>] [-o <format>]
|
||||
```
|
||||
|
||||
| Option | Type | Required | Description |
|
||||
|--------|------|----------|-------------|
|
||||
| `--data <json>` | string | One of `--data` or `--file` | Resource data as JSON |
|
||||
| `--file <path>` | string | One of `--data` or `--file` | Path to JSON file |
|
||||
| `-o, --output` | string | No | Output format |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime <resource> update`
|
||||
|
||||
Update an existing resource.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> update <id> --data <json> [-o <format>]
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<id>` | argument | Yes | Resource ID |
|
||||
| `--data <json>` | option | Yes | Fields to update as JSON |
|
||||
| `-o, --output` | option | No | Output format |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime <resource> delete`
|
||||
|
||||
Delete a resource.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> delete <id> [--force]
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `<id>` | argument | Yes | Resource ID |
|
||||
| `--force` | option | No | Skip confirmation prompt |
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime <resource> count`
|
||||
|
||||
Count resources matching a filter.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> count [--query <json>]
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `--query <json>` | string | None | Filter criteria as JSON |
|
||||
|
||||
---
|
||||
|
||||
## Utility Commands
|
||||
|
||||
### `oneuptime version`
|
||||
|
||||
Display the CLI version.
|
||||
|
||||
```bash
|
||||
oneuptime version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime whoami`
|
||||
|
||||
Show current authentication details.
|
||||
|
||||
```bash
|
||||
oneuptime whoami
|
||||
```
|
||||
|
||||
Displays the instance URL and masked API key. If a saved context is active, the context name is also shown.
|
||||
|
||||
---
|
||||
|
||||
### `oneuptime resources`
|
||||
|
||||
List all available resource types.
|
||||
|
||||
```bash
|
||||
oneuptime resources [--type <type>]
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `--type <type>` | string | None | Filter by `database` or `analytics` |
|
||||
|
||||
---
|
||||
|
||||
## Global Options
|
||||
|
||||
These flags are available on all commands:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--api-key <key>` | Override API key |
|
||||
| `--url <url>` | Override instance URL |
|
||||
| `--context <name>` | Use a specific context |
|
||||
| `-o, --output <format>` | Output format: `json`, `table`, `wide` |
|
||||
| `--no-color` | Disable colored output |
|
||||
| `--help` | Show help |
|
||||
| `--version` | Show version |
|
||||
|
||||
## API Routes
|
||||
|
||||
For reference, the CLI maps commands to these API endpoints:
|
||||
|
||||
| Command | Method | Endpoint |
|
||||
|---------|--------|----------|
|
||||
| `list` | POST | `/api/<resource>/get-list` |
|
||||
| `get` | POST | `/api/<resource>/<id>/get-item` |
|
||||
| `create` | POST | `/api/<resource>` |
|
||||
| `update` | PUT | `/api/<resource>/<id>/` |
|
||||
| `delete` | DELETE | `/api/<resource>/<id>/` |
|
||||
| `count` | POST | `/api/<resource>/count` |
|
||||
|
||||
All requests include the `APIKey` header for authentication.
|
||||
68
Docs/Content/cli/index.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# OneUptime CLI
|
||||
|
||||
The OneUptime CLI is a command-line interface for managing your OneUptime resources directly from the terminal. It supports full CRUD operations on monitors, incidents, alerts, status pages, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-environment support** with named contexts for production, staging, and development
|
||||
- **Auto-discovery** of available resources from your OneUptime instance
|
||||
- **Flexible authentication** via CLI flags, environment variables, or saved contexts
|
||||
- **Smart output formatting** with JSON, table, and wide display modes
|
||||
- **Scriptable** for CI/CD pipelines and automation workflows
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @oneuptime/cli
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate with your OneUptime instance
|
||||
oneuptime login <your-api-key> https://oneuptime.com
|
||||
|
||||
# List your monitors
|
||||
oneuptime monitor list
|
||||
|
||||
# View a specific incident
|
||||
oneuptime incident get <incident-id>
|
||||
|
||||
# See all available resources
|
||||
oneuptime resources
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Authentication](./authentication.md) | Login, contexts, and credential management |
|
||||
| [Resource Operations](./resource-operations.md) | CRUD operations on monitors, incidents, alerts, and more |
|
||||
| [Output Formats](./output-formats.md) | JSON, table, and wide output modes |
|
||||
| [Scripting and CI/CD](./scripting.md) | Automation, environment variables, and pipeline usage |
|
||||
| [Command Reference](./command-reference.md) | Complete reference for all commands and options |
|
||||
|
||||
## Global Options
|
||||
|
||||
These flags can be used with any command:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key <key>` | Override API key for this command |
|
||||
| `--url <url>` | Override instance URL for this command |
|
||||
| `--context <name>` | Use a specific named context |
|
||||
| `-o, --output <format>` | Output format: `json`, `table`, `wide` |
|
||||
| `--no-color` | Disable colored output |
|
||||
| `--help` | Show command help |
|
||||
| `--version` | Show CLI version |
|
||||
|
||||
## Getting Help
|
||||
|
||||
```bash
|
||||
# General help
|
||||
oneuptime --help
|
||||
|
||||
# Help for a specific command
|
||||
oneuptime monitor --help
|
||||
oneuptime monitor list --help
|
||||
```
|
||||
80
Docs/Content/cli/output-formats.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Output Formats
|
||||
|
||||
The OneUptime CLI supports three output formats: **table**, **JSON**, and **wide**. You can set the format with the `-o` or `--output` flag on any command.
|
||||
|
||||
## Table (Default)
|
||||
|
||||
The default format when running in an interactive terminal. Displays results as an ASCII table with intelligently selected columns.
|
||||
|
||||
```bash
|
||||
oneuptime incident list
|
||||
```
|
||||
|
||||
```
|
||||
┌──────────────────┬───────────────────────┬─────────────────────┬─────────────────────┐
|
||||
│ _id │ title │ createdAt │ updatedAt │
|
||||
├──────────────────┼───────────────────────┼─────────────────────┼─────────────────────┤
|
||||
│ abc-123 │ API Outage │ 2025-01-15T10:30:00 │ 2025-01-15T12:00:00 │
|
||||
│ def-456 │ Database Slowdown │ 2025-01-14T08:15:00 │ 2025-01-14T09:30:00 │
|
||||
└──────────────────┴───────────────────────┴─────────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
Table format behavior:
|
||||
- Selects up to 6 columns, prioritizing: `_id`, `name`, `title`, `createdAt`, `updatedAt`
|
||||
- Truncates values longer than 60 characters with `...`
|
||||
- Uses color-coded headers (disable with `--no-color`)
|
||||
|
||||
## JSON
|
||||
|
||||
Raw JSON output, pretty-printed with 2-space indentation. This is the best format for scripting and piping to other tools.
|
||||
|
||||
```bash
|
||||
oneuptime incident list -o json
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"_id": "abc-123",
|
||||
"title": "API Outage",
|
||||
"currentIncidentStateId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"createdAt": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
JSON format is automatically used when the output is piped to another command (non-TTY mode):
|
||||
|
||||
```bash
|
||||
# JSON is used automatically when piping
|
||||
oneuptime incident list | jq '.[].title'
|
||||
```
|
||||
|
||||
## Wide
|
||||
|
||||
Displays all columns without truncation. Useful for detailed inspection but may produce very wide output.
|
||||
|
||||
```bash
|
||||
oneuptime incident list -o wide
|
||||
```
|
||||
|
||||
## Disabling Color
|
||||
|
||||
Color output can be disabled in several ways:
|
||||
|
||||
```bash
|
||||
# Using the --no-color flag
|
||||
oneuptime --no-color incident list
|
||||
|
||||
# Using the NO_COLOR environment variable
|
||||
NO_COLOR=1 oneuptime incident list
|
||||
```
|
||||
|
||||
## Special Output Cases
|
||||
|
||||
| Scenario | Output |
|
||||
|----------|--------|
|
||||
| Empty result set | `"No results found."` |
|
||||
| No data returned | `"No data returned."` |
|
||||
| Single object (e.g., `get`) | Key-value table format |
|
||||
| `count` command | Plain numeric value |
|
||||
230
Docs/Content/cli/resource-operations.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Resource Operations
|
||||
|
||||
The OneUptime CLI provides full CRUD (Create, Read, Update, Delete) operations for all supported resources. Resources are auto-discovered from your OneUptime instance.
|
||||
|
||||
## Available Resources
|
||||
|
||||
Run the following command to see all available resource types:
|
||||
|
||||
```bash
|
||||
oneuptime resources
|
||||
```
|
||||
|
||||
You can filter by type:
|
||||
|
||||
```bash
|
||||
# Show only database resources
|
||||
oneuptime resources --type database
|
||||
|
||||
# Show only analytics resources
|
||||
oneuptime resources --type analytics
|
||||
```
|
||||
|
||||
Common resources include:
|
||||
|
||||
| Resource | Command |
|
||||
|----------|---------|
|
||||
| Incident | `oneuptime incident` |
|
||||
| Alert | `oneuptime alert` |
|
||||
| Monitor | `oneuptime monitor` |
|
||||
| Monitor Status | `oneuptime monitor-status` |
|
||||
| Incident State | `oneuptime incident-state` |
|
||||
| Status Page | `oneuptime status-page` |
|
||||
| On-Call Policy | `oneuptime on-call-policy` |
|
||||
| Team | `oneuptime team` |
|
||||
| Scheduled Maintenance Event | `oneuptime scheduled-maintenance-event` |
|
||||
|
||||
## List Resources
|
||||
|
||||
Retrieve a list of resources with optional filtering, pagination, and sorting.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> list [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--query <json>` | Filter criteria as JSON | None |
|
||||
| `--limit <n>` | Maximum number of results | `10` |
|
||||
| `--skip <n>` | Number of results to skip | `0` |
|
||||
| `--sort <json>` | Sort order as JSON | None |
|
||||
| `-o, --output <format>` | Output format | `table` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# List the 10 most recent incidents
|
||||
oneuptime incident list
|
||||
|
||||
# Filter incidents by state ID
|
||||
oneuptime incident list --query '{"currentIncidentStateId":"<state-id>"}'
|
||||
|
||||
# List with pagination
|
||||
oneuptime incident list --limit 20 --skip 40
|
||||
|
||||
# Sort by creation date (descending)
|
||||
oneuptime incident list --sort '{"createdAt":-1}'
|
||||
|
||||
# Output as JSON
|
||||
oneuptime incident list -o json
|
||||
```
|
||||
|
||||
## Get a Resource
|
||||
|
||||
Retrieve a single resource by its ID.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> get <id>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<id>` | The resource ID (UUID) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Get a specific incident
|
||||
oneuptime incident get 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
# Get a monitor as JSON
|
||||
oneuptime monitor get abc-123 -o json
|
||||
```
|
||||
|
||||
## Create a Resource
|
||||
|
||||
Create a new resource from inline JSON or a file.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> create [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--data <json>` | Resource data as a JSON object |
|
||||
| `--file <path>` | Path to a JSON file containing resource data |
|
||||
| `-o, --output <format>` | Output format |
|
||||
|
||||
You must provide either `--data` or `--file`.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Create an incident with inline JSON
|
||||
oneuptime incident create --data '{"title":"API Outage","currentIncidentStateId":"<state-id>","incidentSeverityId":"<severity-id>","declaredAt":"2025-01-15T10:30:00Z"}'
|
||||
|
||||
# Create from a JSON file
|
||||
oneuptime incident create --file incident.json
|
||||
|
||||
# Create and output as JSON to capture the ID
|
||||
oneuptime monitor create --data '{"name":"API Health Check"}' -o json
|
||||
```
|
||||
|
||||
## Update a Resource
|
||||
|
||||
Update an existing resource by ID.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> update <id> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<id>` | The resource ID |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--data <json>` | Fields to update as JSON (required) |
|
||||
| `-o, --output <format>` | Output format |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Change incident state (e.g., to resolved)
|
||||
oneuptime incident update abc-123 --data '{"currentIncidentStateId":"<resolved-state-id>"}'
|
||||
|
||||
# Rename a monitor
|
||||
oneuptime monitor update abc-123 --data '{"name":"Updated Monitor Name"}'
|
||||
```
|
||||
|
||||
## Delete a Resource
|
||||
|
||||
Delete a resource by ID.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> delete <id> [--force]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<id>` | The resource ID |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
oneuptime incident delete abc-123
|
||||
oneuptime monitor delete 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
# Skip confirmation
|
||||
oneuptime monitor delete 550e8400-e29b-41d4-a716-446655440000 --force
|
||||
```
|
||||
|
||||
## Count Resources
|
||||
|
||||
Count resources matching optional filter criteria.
|
||||
|
||||
```bash
|
||||
oneuptime <resource> count [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--query <json>` | Filter criteria as JSON |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Count all incidents
|
||||
oneuptime incident count
|
||||
|
||||
# Count incidents by state
|
||||
oneuptime incident count --query '{"currentIncidentStateId":"<state-id>"}'
|
||||
|
||||
# Count monitors
|
||||
oneuptime monitor count
|
||||
```
|
||||
|
||||
## Analytics Resources
|
||||
|
||||
Analytics resources support a limited set of operations compared to database resources:
|
||||
|
||||
| Operation | Supported |
|
||||
|-----------|-----------|
|
||||
| `list` | Yes |
|
||||
| `create` | Yes |
|
||||
| `count` | Yes |
|
||||
| `get` | No |
|
||||
| `update` | No |
|
||||
| `delete` | No |
|
||||
|
||||
Use `oneuptime resources --type analytics` to see which analytics resources are available on your instance.
|
||||
152
Docs/Content/cli/scripting.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Scripting and CI/CD
|
||||
|
||||
The OneUptime CLI is designed for automation. It supports environment-variable-based authentication, JSON output for programmatic parsing, and appropriate exit codes for pipeline integration.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set these environment variables to authenticate without saved contexts:
|
||||
|
||||
```bash
|
||||
export ONEUPTIME_API_KEY=sk-your-api-key
|
||||
export ONEUPTIME_URL=https://oneuptime.com
|
||||
```
|
||||
|
||||
These take precedence over saved contexts but are overridden by CLI flags.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `0` | Success |
|
||||
| `1` | General error |
|
||||
| `2` | Authentication error (missing or invalid credentials) |
|
||||
| `3` | Not found (404) |
|
||||
|
||||
Use exit codes in scripts to handle errors:
|
||||
|
||||
```bash
|
||||
if ! oneuptime monitor list > /dev/null 2>&1; then
|
||||
echo "Failed to list monitors"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## JSON Processing with jq
|
||||
|
||||
Use `-o json` to produce machine-readable output:
|
||||
|
||||
```bash
|
||||
# Extract all incident titles
|
||||
oneuptime incident list -o json | jq '.[].title'
|
||||
|
||||
# Get the ID of a newly created monitor
|
||||
NEW_ID=$(oneuptime monitor create --data '{"name":"API Health"}' -o json | jq -r '._id')
|
||||
echo "Created monitor: $NEW_ID"
|
||||
|
||||
# Count incidents by severity
|
||||
oneuptime incident count --query '{"incidentSeverityId":"<severity-id>"}'
|
||||
```
|
||||
|
||||
## Creating Resources from Files
|
||||
|
||||
Use `--file` to create resources from JSON files, useful for version-controlled infrastructure:
|
||||
|
||||
```bash
|
||||
# monitor.json
|
||||
# {
|
||||
# "name": "API Health Check",
|
||||
# "projectId": "your-project-id"
|
||||
# }
|
||||
|
||||
oneuptime monitor create --file monitor.json
|
||||
```
|
||||
|
||||
## Batch Operations
|
||||
|
||||
Process multiple resources in a loop:
|
||||
|
||||
```bash
|
||||
# Create multiple monitors from a JSON array file
|
||||
cat monitors.json | jq -r '.[] | @json' | while read monitor; do
|
||||
oneuptime monitor create --data "$monitor"
|
||||
done
|
||||
```
|
||||
|
||||
## CI/CD Pipeline Examples
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Check Active Incidents
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/5 * * * *'
|
||||
|
||||
jobs:
|
||||
health-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install OneUptime CLI
|
||||
run: npm install -g @oneuptime/cli
|
||||
|
||||
- name: Check for active incidents
|
||||
env:
|
||||
ONEUPTIME_API_KEY: ${{ secrets.ONEUPTIME_API_KEY }}
|
||||
ONEUPTIME_URL: https://oneuptime.com
|
||||
run: |
|
||||
INCIDENT_COUNT=$(oneuptime incident count)
|
||||
if [ "$INCIDENT_COUNT" -gt 0 ]; then
|
||||
echo "WARNING: $INCIDENT_COUNT incidents found"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Generic CI/CD Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export ONEUPTIME_API_KEY="$CI_ONEUPTIME_API_KEY"
|
||||
export ONEUPTIME_URL="$CI_ONEUPTIME_URL"
|
||||
|
||||
# Create a deployment incident and capture the ID
|
||||
# Note: currentIncidentStateId and incidentSeverityId must reference existing state/severity IDs in your project
|
||||
INCIDENT_ID=$(oneuptime incident create --data '{
|
||||
"title": "Deployment Started",
|
||||
"currentIncidentStateId": "'"$INVESTIGATING_STATE_ID"'",
|
||||
"incidentSeverityId": "'"$SEVERITY_ID"'",
|
||||
"declaredAt": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
|
||||
}' -o json | jq -r '._id')
|
||||
|
||||
# Run deployment steps here...
|
||||
|
||||
# Resolve the incident after successful deployment
|
||||
oneuptime incident update "$INCIDENT_ID" --data '{"currentIncidentStateId":"'"$RESOLVED_STATE_ID"'"}'
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-slim
|
||||
RUN npm install -g @oneuptime/cli
|
||||
ENV ONEUPTIME_API_KEY=""
|
||||
ENV ONEUPTIME_URL=""
|
||||
ENTRYPOINT ["oneuptime"]
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-e ONEUPTIME_API_KEY=sk-abc123 \
|
||||
-e ONEUPTIME_URL=https://oneuptime.com \
|
||||
oneuptime-cli incident list
|
||||
```
|
||||
|
||||
## Using a Specific Context in Scripts
|
||||
|
||||
If you have multiple contexts saved, target a specific one:
|
||||
|
||||
```bash
|
||||
oneuptime --context production incident list
|
||||
oneuptime --context staging monitor count
|
||||
```
|
||||
@@ -130,6 +130,35 @@ const DocsNav: NavGroup[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "CLI",
|
||||
links: [
|
||||
{
|
||||
title: "Overview",
|
||||
url: "/docs/cli/index",
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
url: "/docs/cli/authentication",
|
||||
},
|
||||
{
|
||||
title: "Resource Operations",
|
||||
url: "/docs/cli/resource-operations",
|
||||
},
|
||||
{
|
||||
title: "Output Formats",
|
||||
url: "/docs/cli/output-formats",
|
||||
},
|
||||
{
|
||||
title: "Scripting & CI/CD",
|
||||
url: "/docs/cli/scripting",
|
||||
},
|
||||
{
|
||||
title: "Command Reference",
|
||||
url: "/docs/cli/command-reference",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Monitor",
|
||||
links: [
|
||||
|
||||
@@ -203,6 +203,12 @@ Usage:
|
||||
- name: VAPID_PRIVATE_KEY
|
||||
value: {{ $.Values.vapid.privateKey }}
|
||||
|
||||
- name: EXPO_ACCESS_TOKEN
|
||||
value: {{ default "" $.Values.expo.accessToken | quote }}
|
||||
|
||||
- name: PUSH_NOTIFICATION_RELAY_URL
|
||||
value: {{ default "https://oneuptime.com/api/notification/push-relay/send" $.Values.pushNotification.relayUrl | quote }}
|
||||
|
||||
- name: SLACK_APP_CLIENT_SECRET
|
||||
value: {{ $.Values.slackApp.clientSecret }}
|
||||
|
||||
|
||||
@@ -73,12 +73,23 @@ spec:
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ $.Values.image.pullPolicy }}
|
||||
env:
|
||||
{{- include "oneuptime.env.common" . | nindent 12 }}
|
||||
{{- include "oneuptime.env.oneuptimeSecret" . | nindent 12 }}
|
||||
- name: PORT
|
||||
value: {{ $.Values.isolatedVM.ports.http | quote }}
|
||||
- name: LOG_LEVEL
|
||||
value: {{ $.Values.logLevel }}
|
||||
- name: NODE_ENV
|
||||
value: {{ $.Values.nodeEnvironment }}
|
||||
- name: DISABLE_TELEMETRY
|
||||
value: {{ $.Values.isolatedVM.disableTelemetryCollection | quote }}
|
||||
{{- if $.Values.openTelemetryExporter.endpoint }}
|
||||
- name: OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT
|
||||
value: {{ $.Values.openTelemetryExporter.endpoint }}
|
||||
{{- end }}
|
||||
{{- if $.Values.openTelemetryExporter.headers }}
|
||||
- name: OPENTELEMETRY_EXPORTER_OTLP_HEADERS
|
||||
value: {{ $.Values.openTelemetryExporter.headers }}
|
||||
{{- end }}
|
||||
|
||||
ports:
|
||||
- containerPort: {{ $.Values.isolatedVM.ports.http }}
|
||||
|
||||
@@ -131,7 +131,7 @@ spec:
|
||||
- name: NO_PROXY
|
||||
value: {{ $val.proxy.noProxy | squote }}
|
||||
{{- end }}
|
||||
{{- include "oneuptime.env.runtime" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
|
||||
{{- include "oneuptime.env.oneuptimeSecret" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: {{ if and $val.ports $val.ports.http }}{{ $val.ports.http }}{{ else }}3874{{ end }}
|
||||
protocol: TCP
|
||||
|
||||
@@ -727,6 +727,24 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"expo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accessToken": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"pushNotification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"relayUrl": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"incidents": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -287,6 +287,15 @@ vapid:
|
||||
privateKey:
|
||||
subject: mailto:support@oneuptime.com
|
||||
|
||||
# Expo access token for sending mobile push notifications directly via Expo SDK.
|
||||
# If not set, notifications are relayed through the push notification relay URL.
|
||||
expo:
|
||||
accessToken:
|
||||
|
||||
# Push notification relay URL for self-hosted instances without Expo credentials
|
||||
pushNotification:
|
||||
relayUrl: https://oneuptime.com/api/notification/push-relay/send
|
||||
|
||||
incidents:
|
||||
disableAutomaticCreation: false
|
||||
|
||||
|
||||
@@ -1257,6 +1257,31 @@ const HomeFeatureSet: FeatureSet = {
|
||||
res.redirect("/product/ai-agent");
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/tool/mcp-server",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/tool/mcp-server",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/mcp-server`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.get("/tool/cli", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
|
||||
"/tool/cli",
|
||||
res.locals["homeUrl"] as string,
|
||||
);
|
||||
res.render(`${ViewsPath}/cli`, {
|
||||
enableGoogleTagManager: IsBillingEnabled,
|
||||
seo,
|
||||
});
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/enterprise/overview",
|
||||
(_req: ExpressRequest, res: ExpressResponse) => {
|
||||
|
||||
@@ -305,6 +305,72 @@ export const PageSEOConfig: Record<string, PageSEOData> = {
|
||||
},
|
||||
},
|
||||
|
||||
"/tool/mcp-server": {
|
||||
title: "MCP Server | Model Context Protocol for AI Agents | OneUptime",
|
||||
description:
|
||||
"Connect AI agents and LLMs to your OneUptime observability data via Model Context Protocol (MCP). Query incidents, monitors, logs, metrics, and traces directly from your AI tools.",
|
||||
canonicalPath: "/tool/mcp-server",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "MCP Server", url: "/tool/mcp-server" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime MCP Server",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "Web, Cloud",
|
||||
description:
|
||||
"Model Context Protocol server that connects AI agents and LLMs to OneUptime observability data for querying incidents, monitors, logs, metrics, and traces.",
|
||||
features: [
|
||||
"Incident querying and management",
|
||||
"Monitor status and health checks",
|
||||
"Log search and filtering",
|
||||
"Metrics time series retrieval",
|
||||
"Distributed trace analysis",
|
||||
"Compatible with Claude, Cursor, Windsurf",
|
||||
"API key authentication",
|
||||
"Fine-grained permissions",
|
||||
"Real-time data access",
|
||||
"Open protocol standard",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/tool/cli": {
|
||||
title: "CLI | Command Line Interface for Observability | OneUptime",
|
||||
description:
|
||||
"OneUptime CLI lets you manage monitors, incidents, status pages, and observability data from your terminal. Deploy, configure, and automate your monitoring infrastructure with simple commands.",
|
||||
canonicalPath: "/tool/cli",
|
||||
twitterCard: "summary_large_image",
|
||||
pageType: "product",
|
||||
breadcrumbs: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Products", url: "/#products" },
|
||||
{ name: "CLI", url: "/tool/cli" },
|
||||
],
|
||||
softwareApplication: {
|
||||
name: "OneUptime CLI",
|
||||
applicationCategory: "DeveloperApplication",
|
||||
operatingSystem: "macOS, Linux, Windows",
|
||||
description:
|
||||
"Command line interface for managing OneUptime monitors, incidents, status pages, and observability data from your terminal.",
|
||||
features: [
|
||||
"Monitor creation and management",
|
||||
"Incident response from terminal",
|
||||
"Status page management",
|
||||
"Real-time log tailing",
|
||||
"CI/CD pipeline integration",
|
||||
"Scriptable JSON output",
|
||||
"YAML configuration support",
|
||||
"Bulk operations",
|
||||
"npm, Homebrew, and Docker install",
|
||||
"API key and browser authentication",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"/product/metrics": {
|
||||
title: "Metrics | Application & Infrastructure Metrics | OneUptime",
|
||||
description:
|
||||
|
||||
547
Home/Views/cli.ejs
Normal file
@@ -0,0 +1,547 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" id="home">
|
||||
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
|
||||
<head>
|
||||
<title>OneUptime | CLI - Command Line Interface for Observability</title>
|
||||
<meta name="description"
|
||||
content="OneUptime CLI lets you manage monitors, incidents, status pages, and observability data from your terminal. Deploy, configure, and automate your monitoring infrastructure with simple commands.">
|
||||
<%- include('head', {
|
||||
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
|
||||
}) -%>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('nav') -%>
|
||||
<main id="main-content">
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="relative isolate overflow-hidden bg-white" id="cli-hero-section">
|
||||
<!-- Subtle grid pattern background -->
|
||||
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
|
||||
|
||||
<!-- Grid glow effect that follows cursor -->
|
||||
<div id="cli-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(16, 185, 129, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(16, 185, 129, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
|
||||
|
||||
<div class="py-20 sm:py-28 lg:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<!-- Minimal badge -->
|
||||
<p class="text-sm font-medium text-emerald-600 mb-4">Command Line Interface</p>
|
||||
|
||||
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
|
||||
Observability from your terminal
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
||||
Manage monitors, incidents, status pages, and your entire observability stack from the command line. Automate workflows, integrate with CI/CD, and stay in your terminal.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="/accounts/register"
|
||||
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
</a>
|
||||
<a href="/docs/cli" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
|
||||
Read docs <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Subtle feature list -->
|
||||
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
|
||||
<span>Monitor management</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Incident response</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>CI/CD integration</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Scriptable</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal mockup -->
|
||||
<div class="mt-16 sm:mt-20">
|
||||
<div class="relative mx-auto max-w-3xl">
|
||||
<div class="rounded-xl bg-gray-900 p-1.5 ring-1 ring-gray-900/10 shadow-2xl">
|
||||
<!-- Terminal header -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
|
||||
</div>
|
||||
<!-- Terminal content -->
|
||||
<div class="p-6 font-mono text-sm space-y-3">
|
||||
<div>
|
||||
<span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor list</span>
|
||||
</div>
|
||||
<div class="text-gray-400 pl-2 space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-emerald-400">●</span>
|
||||
<span class="text-gray-300 w-40">API Gateway</span>
|
||||
<span class="text-emerald-400">Up</span>
|
||||
<span class="text-gray-500">99.98%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-emerald-400">●</span>
|
||||
<span class="text-gray-300 w-40">Web App</span>
|
||||
<span class="text-emerald-400">Up</span>
|
||||
<span class="text-gray-500">99.95%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-red-400">●</span>
|
||||
<span class="text-gray-300 w-40">Payment Service</span>
|
||||
<span class="text-red-400">Down</span>
|
||||
<span class="text-gray-500">98.50%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-emerald-400">●</span>
|
||||
<span class="text-gray-300 w-40">Database</span>
|
||||
<span class="text-emerald-400">Up</span>
|
||||
<span class="text-gray-500">99.99%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident create --title "Payment Service Down" --severity critical</span>
|
||||
</div>
|
||||
<div class="text-emerald-400 pl-2">
|
||||
Incident INC-2847 created successfully. On-call team notified.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('logo-roll') -%>
|
||||
|
||||
<!-- How It Works -->
|
||||
<div class="relative bg-gray-50 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<p class="text-sm font-medium text-emerald-600 uppercase tracking-wide mb-3">Get Started</p>
|
||||
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Up and running in seconds
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
Install, authenticate, and start managing your infrastructure from the terminal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<!-- Connecting line for desktop -->
|
||||
<div class="hidden lg:block relative">
|
||||
<div class="absolute top-8 left-[calc(16.67%+24px)] right-[calc(16.67%+24px)] h-px bg-emerald-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Step 1 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">1</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Install</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Install the CLI via npm, Homebrew, or download the binary directly.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">2</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Authenticate</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Login with your API key or authenticate via the browser.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">3</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Run</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Start managing your monitors, incidents, and observability data from the terminal.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why CLI Section -->
|
||||
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!-- Section Header -->
|
||||
<div class="mx-auto max-w-2xl text-center mb-20">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-4 py-1.5 text-sm font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 mb-6">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
Why CLI
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
|
||||
Power users love the terminal
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
Everything you can do in the dashboard, now available as commands you can script, pipe, and automate.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature Blocks -->
|
||||
<div class="space-y-24 lg:space-y-32">
|
||||
<!-- Feature 1: Monitor Management -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
|
||||
<svg class="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-emerald-600 uppercase tracking-wide">Monitor Management</span>
|
||||
</div>
|
||||
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Create and Manage Monitors</h3>
|
||||
<p class="text-lg text-gray-600 mb-6">
|
||||
Create, update, and manage monitors directly from the command line. List monitor statuses, view uptime history, and configure alerts without opening a browser.
|
||||
</p>
|
||||
<ul class="space-y-3 mb-8">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Create monitors from YAML configs
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
View real-time status and uptime
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Bulk operations for large setups
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Terminal Mockup -->
|
||||
<div class="relative bg-gray-900 rounded-xl shadow-lg overflow-hidden max-w-md mx-auto ring-1 ring-gray-700/50">
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 font-mono text-xs space-y-2">
|
||||
<div><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor create \</span></div>
|
||||
<div class="text-gray-300 pl-4">--name "API Health Check" \</div>
|
||||
<div class="text-gray-300 pl-4">--type http \</div>
|
||||
<div class="text-gray-300 pl-4">--url https://api.example.com/health \</div>
|
||||
<div class="text-gray-300 pl-4">--interval 30s</div>
|
||||
<div class="mt-2 text-emerald-400">Monitor created: mon_8x7k2m9p</div>
|
||||
<div class="text-gray-500">Type: HTTP | Interval: 30s | Status: Active</div>
|
||||
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor status mon_8x7k2m9p</span></div>
|
||||
<div class="mt-1 text-emerald-400">● Up - 200 OK (45ms)</div>
|
||||
<div class="text-gray-500">Last checked: 12s ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2: Incident Response -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative order-2 lg:order-1 mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Terminal Mockup -->
|
||||
<div class="relative bg-gray-900 rounded-xl shadow-lg overflow-hidden max-w-md mx-auto ring-1 ring-gray-700/50">
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 font-mono text-xs space-y-2">
|
||||
<div><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident list --state active</span></div>
|
||||
<div class="mt-1 space-y-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-red-400">●</span>
|
||||
<span class="text-gray-300">INC-2847</span>
|
||||
<span class="text-red-400 text-[10px] px-1.5 py-0.5 bg-red-900/30 rounded">Critical</span>
|
||||
<span class="text-gray-400">Payment Service Down</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-amber-400">●</span>
|
||||
<span class="text-gray-300">INC-2846</span>
|
||||
<span class="text-amber-400 text-[10px] px-1.5 py-0.5 bg-amber-900/30 rounded">Warning</span>
|
||||
<span class="text-gray-400">High latency on DB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident acknowledge INC-2847</span></div>
|
||||
<div class="text-emerald-400 mt-1">Incident INC-2847 acknowledged.</div>
|
||||
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident resolve INC-2847 \</span></div>
|
||||
<div class="text-gray-300 pl-4">--note "Restarted payment service pod"</div>
|
||||
<div class="text-emerald-400 mt-1">Incident INC-2847 resolved.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative order-1 lg:order-2">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
|
||||
<svg class="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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.126z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-emerald-600 uppercase tracking-wide">Incident Response</span>
|
||||
</div>
|
||||
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Respond to Incidents Faster</h3>
|
||||
<p class="text-lg text-gray-600 mb-6">
|
||||
Acknowledge, investigate, and resolve incidents without leaving your terminal. Add notes, change severity, and notify your team all from the command line.
|
||||
</p>
|
||||
<ul class="space-y-3 mb-8">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Acknowledge and resolve incidents
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Add investigation notes
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Trigger on-call escalations
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid Section -->
|
||||
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-4 py-1.5 text-sm font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 mb-6">
|
||||
<svg class="h-4 w-4 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-emerald-700">Capabilities</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Everything you need in the terminal
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
A complete command-line toolkit for managing your observability infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
|
||||
<svg class="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Monitor Management</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Create, list, update, and delete monitors. Support for HTTP, TCP, UDP, ping, and custom monitors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-red-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-red-50 ring-1 ring-red-200">
|
||||
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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.126z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Incident Response</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Create, acknowledge, and resolve incidents. Add notes, change severity, and manage the full incident lifecycle.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
|
||||
<svg class="h-6 w-6 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Status Pages</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Manage status pages, update component statuses, and post status updates from the command line.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-amber-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-50 ring-1 ring-amber-200">
|
||||
<svg class="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Log Tailing</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Tail logs in real-time, search with filters, and pipe output to other tools for advanced analysis.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-50 ring-1 ring-violet-200">
|
||||
<svg class="h-6 w-6 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">CI/CD Integration</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Integrate with GitHub Actions, GitLab CI, Jenkins, and more. Automate monitor creation on deployment.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-indigo-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-indigo-50 ring-1 ring-indigo-200">
|
||||
<svg class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Scriptable Output</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">JSON output mode for scripting. Pipe output to jq, grep, or your own tools for custom workflows.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install Section -->
|
||||
<div class="relative bg-gray-900 py-24 sm:py-32 overflow-hidden">
|
||||
<div class="relative mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 backdrop-blur border border-white/20 mb-6">
|
||||
<svg class="h-4 w-4 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-white">Installation</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-white sm:text-4xl lg:text-5xl">
|
||||
Install in one command
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-300">
|
||||
Choose your preferred package manager and get started in seconds.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-300">npm</span>
|
||||
</div>
|
||||
<div class="font-mono text-sm text-emerald-400">
|
||||
npm install -g oneuptime-cli
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-300">Homebrew</span>
|
||||
</div>
|
||||
<div class="font-mono text-sm text-emerald-400">
|
||||
brew install oneuptime/tap/oneuptime
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-300">Docker</span>
|
||||
</div>
|
||||
<div class="font-mono text-sm text-emerald-400">
|
||||
docker run --rm oneuptime/cli monitor list
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('./Partials/enterprise-ready') -%>
|
||||
<%- include('features-table') -%>
|
||||
<%- include('cta') -%>
|
||||
</main>
|
||||
<%- include('footer') -%>
|
||||
<%- include('./Partials/video-script') -%>
|
||||
|
||||
<script>
|
||||
// Grid glow effect for CLI hero section
|
||||
(function() {
|
||||
const heroSection = document.getElementById('cli-hero-section');
|
||||
const gridGlow = document.getElementById('cli-grid-glow');
|
||||
|
||||
if (heroSection && gridGlow) {
|
||||
heroSection.addEventListener('mousemove', (e) => {
|
||||
const rect = heroSection.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
gridGlow.style.setProperty('--mouse-x', `${x}px`);
|
||||
gridGlow.style.setProperty('--mouse-y', `${y}px`);
|
||||
gridGlow.style.opacity = '1';
|
||||
});
|
||||
|
||||
heroSection.addEventListener('mouseleave', () => {
|
||||
gridGlow.style.opacity = '0';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -133,6 +133,12 @@
|
||||
<li><a href="/solutions/platform" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">Platform</a></li>
|
||||
<li><a href="/solutions/developers" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">Developers</a></li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-sm font-semibold text-gray-900 mt-8">Tools</h3>
|
||||
<ul role="list" class="mt-4 space-y-3">
|
||||
<li><a href="/tool/mcp-server" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">MCP Server</a></li>
|
||||
<li><a href="/tool/cli" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">CLI</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
607
Home/Views/mcp-server.ejs
Normal file
@@ -0,0 +1,607 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" id="home">
|
||||
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
|
||||
<head>
|
||||
<title>OneUptime | MCP Server - Model Context Protocol for AI Agents</title>
|
||||
<meta name="description"
|
||||
content="Connect AI agents and LLMs to your OneUptime observability data via Model Context Protocol (MCP). Query incidents, monitors, logs, metrics, and traces directly from your AI tools.">
|
||||
<%- include('head', {
|
||||
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
|
||||
}) -%>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('nav') -%>
|
||||
<main id="main-content">
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="relative isolate overflow-hidden bg-white" id="mcp-hero-section">
|
||||
<!-- Subtle grid pattern background -->
|
||||
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
|
||||
|
||||
<!-- Grid glow effect that follows cursor -->
|
||||
<div id="mcp-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(14, 165, 233, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(14, 165, 233, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
|
||||
|
||||
<div class="py-20 sm:py-28 lg:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<!-- Minimal badge -->
|
||||
<p class="text-sm font-medium text-sky-600 mb-4">Model Context Protocol</p>
|
||||
|
||||
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
|
||||
Connect AI agents to your observability data
|
||||
</h1>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
||||
OneUptime MCP Server lets AI agents and LLMs query your incidents, monitors, logs, metrics, and traces directly. Give your AI tools real-time context about your infrastructure.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="/accounts/register"
|
||||
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
</a>
|
||||
<a href="/docs/mcp-server" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
|
||||
Read docs <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Subtle feature list -->
|
||||
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
|
||||
<span>Incidents & Monitors</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Logs & Metrics</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Traces & Spans</span>
|
||||
<span class="hidden sm:inline text-gray-300">|</span>
|
||||
<span>Open Protocol</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal mockup -->
|
||||
<div class="mt-16 sm:mt-20">
|
||||
<div class="relative mx-auto max-w-3xl">
|
||||
<div class="rounded-xl bg-gray-900 p-1.5 ring-1 ring-gray-900/10 shadow-2xl">
|
||||
<!-- Terminal header -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs font-mono ml-2">MCP Server Configuration</span>
|
||||
</div>
|
||||
<!-- Terminal content -->
|
||||
<div class="p-6 font-mono text-sm">
|
||||
<div class="text-gray-400">// Add to your MCP client config</div>
|
||||
<div class="mt-2 text-gray-300">{</div>
|
||||
<div class="text-gray-300 ml-4">"<span class="text-sky-400">mcpServers</span>": {</div>
|
||||
<div class="text-gray-300 ml-8">"<span class="text-sky-400">oneuptime</span>": {</div>
|
||||
<div class="text-gray-300 ml-12">"<span class="text-emerald-400">url</span>": "<span class="text-amber-300">https://your-oneuptime.com/mcp</span>",</div>
|
||||
<div class="text-gray-300 ml-12">"<span class="text-emerald-400">headers</span>": {</div>
|
||||
<div class="text-gray-300 ml-16">"<span class="text-emerald-400">Authorization</span>": "<span class="text-amber-300">Bearer <API_KEY></span>"</div>
|
||||
<div class="text-gray-300 ml-12">}</div>
|
||||
<div class="text-gray-300 ml-8">}</div>
|
||||
<div class="text-gray-300 ml-4">}</div>
|
||||
<div class="text-gray-300">}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('logo-roll') -%>
|
||||
|
||||
<!-- How It Works -->
|
||||
<div class="relative bg-gray-50 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<p class="text-sm font-medium text-sky-600 uppercase tracking-wide mb-3">How It Works</p>
|
||||
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
|
||||
From AI query to actionable insight
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
MCP Server bridges the gap between your AI tools and your observability platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-5xl">
|
||||
<!-- Connecting line for desktop -->
|
||||
<div class="hidden lg:block relative">
|
||||
<div class="absolute top-8 left-[calc(12.5%+24px)] right-[calc(12.5%+24px)] h-px bg-sky-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Step 1 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">1</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Connect</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">Point your AI tool to your OneUptime MCP Server endpoint with your API key.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.625 9.75a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 01.778-.332 48.294 48.294 0 005.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">2</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Query</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">AI agents ask questions about your infrastructure in natural language.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">3</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Retrieve</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">MCP Server fetches real-time data from your OneUptime instance.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 4 -->
|
||||
<div class="text-center">
|
||||
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">4</span>
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">Act</h3>
|
||||
<p class="text-sm text-gray-600 leading-relaxed">AI responds with context-aware insights and can take actions on your behalf.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why OneUptime MCP Section -->
|
||||
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!-- Section Header -->
|
||||
<div class="mx-auto max-w-2xl text-center mb-20">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
|
||||
</svg>
|
||||
Why MCP Server
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
|
||||
Your observability data, AI-ready
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
Give AI agents the full context they need to understand and resolve incidents faster.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature Blocks -->
|
||||
<div class="space-y-24 lg:space-y-32">
|
||||
<!-- Feature 1: Query Incidents -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
|
||||
<svg class="h-5 w-5 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-sky-600 uppercase tracking-wide">Incident Intelligence</span>
|
||||
</div>
|
||||
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Query Incidents in Real-Time</h3>
|
||||
<p class="text-lg text-gray-600 mb-6">
|
||||
AI agents can list active incidents, get incident details, check monitor statuses, and understand the current state of your infrastructure without leaving their workflow.
|
||||
</p>
|
||||
<ul class="space-y-3 mb-8">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
List active and resolved incidents
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Get monitor health status
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
View incident timelines and notes
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- AI Chat Mockup -->
|
||||
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
|
||||
<div class="bg-sky-600 px-4 py-3 flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-white font-semibold text-sm">AI Agent + MCP</div>
|
||||
<div class="text-sky-200 text-xs">Connected to OneUptime</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-4 bg-gray-50">
|
||||
<div class="flex justify-end">
|
||||
<div class="bg-sky-600 text-white px-4 py-2 rounded-2xl rounded-br-md max-w-[80%] text-sm">
|
||||
What incidents are currently active?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<div class="bg-white border border-gray-200 px-4 py-3 rounded-2xl rounded-bl-md max-w-[85%] shadow-sm">
|
||||
<div class="text-sm text-gray-700 space-y-2">
|
||||
<p>Found <span class="font-semibold text-sky-600">2 active incidents</span>:</p>
|
||||
<div class="space-y-2">
|
||||
<div class="bg-red-50 rounded-lg p-2 border border-red-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
||||
<span class="text-xs font-medium text-red-700">Critical</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">API Gateway - High error rate (5xx)</p>
|
||||
</div>
|
||||
<div class="bg-amber-50 rounded-lg p-2 border border-amber-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
|
||||
<span class="text-xs font-medium text-amber-700">Warning</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">Database - High latency detected</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 border-t border-gray-100 bg-white">
|
||||
<div class="flex items-center gap-2 bg-gray-50 rounded-full px-4 py-2 border border-gray-200">
|
||||
<input type="text" placeholder="Ask about your infrastructure..." class="flex-1 bg-transparent text-sm text-gray-600 outline-none" disabled>
|
||||
<button class="w-8 h-8 rounded-full bg-sky-600 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2: Observability Data -->
|
||||
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
|
||||
<div class="relative order-2 lg:order-1 mt-12 lg:mt-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
|
||||
|
||||
<!-- Data Visualization -->
|
||||
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
|
||||
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 font-medium">MCP Tools Available</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-sky-50 border border-sky-100">
|
||||
<div class="w-8 h-8 rounded-lg bg-sky-100 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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.126z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">list_incidents</div>
|
||||
<div class="text-xs text-gray-500">Query active and past incidents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-emerald-50 border border-emerald-100">
|
||||
<div class="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">get_monitor_status</div>
|
||||
<div class="text-xs text-gray-500">Check health of any monitor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-amber-50 border border-amber-100">
|
||||
<div class="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">query_logs</div>
|
||||
<div class="text-xs text-gray-500">Search and filter log entries</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-violet-50 border border-violet-100">
|
||||
<div class="w-8 h-8 rounded-lg bg-violet-100 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 013 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 01-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 01-1.125-1.125V4.125z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">get_metrics</div>
|
||||
<div class="text-xs text-gray-500">Retrieve metric time series data</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-lg bg-rose-50 border border-rose-100">
|
||||
<div class="w-8 h-8 rounded-lg bg-rose-100 flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">search_traces</div>
|
||||
<div class="text-xs text-gray-500">Find and analyze distributed traces</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative order-1 lg:order-2">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
|
||||
<svg class="h-5 w-5 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-sky-600 uppercase tracking-wide">Full Data Access</span>
|
||||
</div>
|
||||
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">All Your Observability Tools</h3>
|
||||
<p class="text-lg text-gray-600 mb-6">
|
||||
MCP Server exposes a rich set of tools that let AI agents access every part of your observability stack - incidents, monitors, logs, metrics, traces, and more.
|
||||
</p>
|
||||
<ul class="space-y-3 mb-8">
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Structured tool definitions
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Type-safe parameters
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-gray-600">
|
||||
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Real-time data access
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
|
||||
Get started
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid Section -->
|
||||
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
|
||||
<svg class="h-4 w-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-sky-700">Capabilities</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||
Built for the AI-native workflow
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
Everything your AI agents need to understand and act on your observability data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
|
||||
<svg class="h-6 w-6 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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.126z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Incident Management</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">List, query, and get details about incidents. AI agents can understand the full context of any incident.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
|
||||
<svg class="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Monitor Status</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Query the health and status of all your monitors. Get uptime data, response times, and alert configurations.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-amber-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-50 ring-1 ring-amber-200">
|
||||
<svg class="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Log Search</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Search and filter logs with full-text search. AI agents can correlate log entries with incidents and traces.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-50 ring-1 ring-violet-200">
|
||||
<svg class="h-6 w-6 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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 013 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 01-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 01-1.125-1.125V4.125z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Metrics Queries</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Retrieve metric time series data. AI agents can analyze trends, detect anomalies, and understand performance patterns.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-rose-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-rose-50 ring-1 ring-rose-200">
|
||||
<svg class="h-6 w-6 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Distributed Traces</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">Search and analyze distributed traces across services. Follow requests through your entire stack.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-indigo-200 hover:shadow-lg">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-indigo-50 ring-1 ring-indigo-200">
|
||||
<svg class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900">Secure by Default</h3>
|
||||
<p class="mt-4 text-gray-600 leading-relaxed">API key authentication with fine-grained permissions. Control exactly what data AI agents can access.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compatible AI Tools -->
|
||||
<div class="relative bg-gray-50 py-24 sm:py-32 overflow-hidden">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl text-center mb-16">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
|
||||
<svg class="h-4 w-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-sky-700">Compatible Tools</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
|
||||
Works with any MCP-compatible client
|
||||
</h2>
|
||||
<p class="mt-6 text-lg leading-8 text-gray-600">
|
||||
Use OneUptime MCP Server with your favorite AI tools and IDE extensions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="grid grid-cols-2 gap-6 sm:grid-cols-4">
|
||||
<div class="group relative">
|
||||
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
|
||||
<div class="text-xl font-bold text-gray-900">Claude</div>
|
||||
<span class="text-sm text-gray-500 mt-1">Desktop & API</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
|
||||
<div class="text-xl font-bold text-gray-900">Cursor</div>
|
||||
<span class="text-sm text-gray-500 mt-1">AI IDE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-blue-200 hover:shadow-lg">
|
||||
<div class="text-xl font-bold text-gray-900">Windsurf</div>
|
||||
<span class="text-sm text-gray-500 mt-1">AI IDE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative">
|
||||
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
|
||||
<div class="text-xl font-bold text-gray-900">Custom</div>
|
||||
<span class="text-sm text-gray-500 mt-1">Any MCP Client</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('./Partials/enterprise-ready') -%>
|
||||
<%- include('features-table') -%>
|
||||
<%- include('cta') -%>
|
||||
</main>
|
||||
<%- include('footer') -%>
|
||||
<%- include('./Partials/video-script') -%>
|
||||
|
||||
<script>
|
||||
// Grid glow effect for MCP hero section
|
||||
(function() {
|
||||
const heroSection = document.getElementById('mcp-hero-section');
|
||||
const gridGlow = document.getElementById('mcp-grid-glow');
|
||||
|
||||
if (heroSection && gridGlow) {
|
||||
heroSection.addEventListener('mousemove', (e) => {
|
||||
const rect = heroSection.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
gridGlow.style.setProperty('--mouse-x', `${x}px`);
|
||||
gridGlow.style.setProperty('--mouse-y', `${y}px`);
|
||||
gridGlow.style.opacity = '1';
|
||||
});
|
||||
|
||||
heroSection.addEventListener('mouseleave', () => {
|
||||
gridGlow.style.opacity = '0';
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6
MCP/package-lock.json
generated
@@ -1297,9 +1297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "OneUptime",
|
||||
"slug": "oneuptime",
|
||||
"slug": "oneuptime-on-call",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
"backgroundColor": "#0D1117"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
@@ -28,10 +28,23 @@
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#0D1117"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.oneuptime.oncall"
|
||||
"edgeToEdgeEnabled": false,
|
||||
"package": "com.oneuptime.oncall",
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"android.permission.USE_BIOMETRIC",
|
||||
"android.permission.USE_FINGERPRINT"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"backgroundColor": "#0D1117",
|
||||
"image": "./assets/splash-icon.png",
|
||||
"imageWidth": 200
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
@@ -43,6 +56,12 @@
|
||||
],
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "d9f87edc-1c3e-466f-b032-1ced7621aa8a"
|
||||
}
|
||||
},
|
||||
"owner": "oneuptime"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 44 KiB |
18
MobileApp/eas.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 3.0.0"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||