Compare commits

...

560 Commits

Author SHA1 Message Date
Nawaz Dhandala
d62816dd49 feat: update probe services to include environment variables for telemetry and logging 2026-02-18 13:50:28 +00:00
Nawaz Dhandala
7dd6129dad feat: add environment variables for log level and node environment in isolated-vm deployment 2026-02-18 13:42:14 +00:00
Nawaz Dhandala
7ccea02340 fix secret in probe 2026-02-18 13:37:22 +00:00
Nawaz Dhandala
0af41725b4 fix: add missing comma in dependencies section of package.json 2026-02-18 13:25:17 +00:00
Nawaz Dhandala
9f6bcddc1e feat: implement default notification rules for verified communication methods in User APIs 2026-02-18 13:16:54 +00:00
Nawaz Dhandala
97c461f7a3 fix: update key generation in MyOnCallPoliciesScreen to handle undefined policyId 2026-02-18 13:08:38 +00:00
Nawaz Dhandala
736f8bb83c chore: update adaptive and regular icons in MobileApp assets 2026-02-18 13:05:25 +00:00
Nawaz Dhandala
eb33daf64f refactor: add expo-splash-screen plugin configuration to app.json 2026-02-18 12:57:19 +00:00
Nawaz Dhandala
c3c90eef03 fix: ensure title and body are defaulted to empty strings in push notification 2026-02-18 12:25:14 +00:00
Nawaz Dhandala
e92e9f08d3 refactor: enhance push notification handling with PushNotificationService integration 2026-02-18 10:31:12 +00:00
Nawaz Dhandala
2b313a7702 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-18 09:58:01 +00:00
Nawaz Dhandala
3cf7c7d1ae refactor: implement push notification relay and enhance Expo integration 2026-02-18 09:56:10 +00:00
Nawaz Dhandala
76cfa7186e refactor: add eas.json configuration for build and submission settings 2026-02-18 09:00:13 +00:00
Simon Larsen
afaff717c0 Merge pull request #2304 from OneUptime/snyk-upgrade-950bdb1d48a0c3f367ba1c51cd0a7dee
[Snyk] Upgrade playwright from 1.57.0 to 1.58.0
2026-02-18 08:57:48 +00:00
Simon Larsen
fde0d5f2c6 Merge pull request #2302 from OneUptime/snyk-fix-68e2c2852a4c507876028cf50ec2c87c
[Snyk] Security upgrade nginx from 1.29.3-alpine to 1.29.5-alpine
2026-02-18 08:57:18 +00:00
Simon Larsen
d5c5387621 Merge branch 'master' into snyk-upgrade-950bdb1d48a0c3f367ba1c51cd0a7dee 2026-02-18 08:56:47 +00:00
Simon Larsen
e0ef6e9a77 Merge pull request #2308 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-18 08:56:11 +00:00
Nawaz Dhandala
2dc0dc4c96 refactor: add google services configuration for Firebase integration 2026-02-18 08:54:10 +00:00
Nawaz Dhandala
c9eb72ba2c refactor: enhance push notification registration with improved error handling and retry logic 2026-02-18 08:39:29 +00:00
Nawaz Dhandala
92e247d168 refactor: update logo size and styling in LoginScreen and ServerUrlScreen for improved visual consistency 2026-02-18 07:57:47 +00:00
Nawaz Dhandala
14988c438a refactor: update app.json slug and add permissions for biometric authentication 2026-02-18 07:48:31 +00:00
Nawaz Dhandala
d81682d02f Refactor styles in various components for improved readability and consistency
- Simplified style definitions in AlertCard, EpisodeCard, FeedTimeline, IncidentCard, AlertDetailScreen, AlertEpisodeDetailScreen, and others by using multi-line formatting.
- Enhanced the layout of components in HomeScreen, MyOnCallPoliciesScreen, and SettingsScreen for better maintainability.
- Updated text styles for better clarity and consistency across screens.
2026-02-18 07:47:07 +00:00
simlarsen
9d5faca3ec chore: npm audit fix 2026-02-18 02:31:53 +00:00
Nawaz Dhandala
89ccde1bc4 refactor: standardize device type strings to lowercase in registerPushDevice function for consistency 2026-02-17 22:12:43 +00:00
Nawaz Dhandala
3aab280dcd refactor: standardize shadow properties and background colors across components for improved consistency 2026-02-17 22:04:56 +00:00
Nawaz Dhandala
b8e44a1bcf refactor: update layout and styling in AddNoteModal and NotesSection for improved consistency and visual clarity 2026-02-17 21:55:20 +00:00
Nawaz Dhandala
4c3b4d23ff refactor: update assignment badge colors and icon in MyOnCallPoliciesScreen for improved visual consistency 2026-02-17 21:42:26 +00:00
Nawaz Dhandala
a4ff718d61 refactor: remove unnecessary shadow properties and simplify layout in MyOnCallPoliciesScreen for improved performance and consistency 2026-02-17 21:40:03 +00:00
Nawaz Dhandala
3433a815f3 refactor: simplify Pressable style and adjust layout properties in HomeScreen for improved accessibility and visual consistency 2026-02-17 21:32:54 +00:00
Nawaz Dhandala
2a20807126 refactor: update padding values in MyOnCallPoliciesScreen for improved layout consistency 2026-02-17 21:31:10 +00:00
Nawaz Dhandala
991dc1c842 refactor: update icon in AlertCard and IncidentCard from desktop-outline to pulse-outline for improved visual representation 2026-02-17 21:29:59 +00:00
Nawaz Dhandala
2026e7fd77 refactor: move marginBottom to parent View in EpisodeCard for improved layout consistency 2026-02-17 21:28:48 +00:00
Nawaz Dhandala
1d0016412e refactor: add marginBottom to SwipeableCard for improved layout spacing 2026-02-17 21:27:38 +00:00
Nawaz Dhandala
917f27fe11 Refactor styles in multiple screens to use inline styles instead of class names for consistency and improved readability. Updated layout properties and adjusted padding, margin, and font sizes across IncidentsScreen, MyOnCallPoliciesScreen, SettingsScreen, LoginScreen, and ServerUrlScreen for better UI alignment and responsiveness. 2026-02-17 21:25:03 +00:00
Nawaz Dhandala
c07c89e3dd refactor: replace Pressable with TouchableOpacity in StatCard for improved touch handling and update layout for better spacing 2026-02-17 21:16:58 +00:00
Nawaz Dhandala
32c4c1666d refactor: update button layout and styles in AlertEpisodeDetailScreen and IncidentEpisodeDetailScreen for consistency 2026-02-17 21:12:52 +00:00
Nawaz Dhandala
636a419cbd refactor: replace Pressable with TouchableOpacity for improved touch handling in SegmentedControl, AlertDetailScreen, and IncidentDetailScreen 2026-02-17 21:10:54 +00:00
Nawaz Dhandala
61699b9f4a refactor: replace Pressable with TouchableOpacity for theme selection options 2026-02-17 21:07:19 +00:00
Nawaz Dhandala
b6ed3643c3 fix: update splash screen background color and replace asset images 2026-02-17 20:57:17 +00:00
Nawaz Dhandala
9e73ac45a1 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-17 15:08:20 +00:00
Nawaz Dhandala
7a3dbd0e8e refactor: streamline style definitions in multiple components for consistency 2026-02-17 14:57:46 +00:00
Nawaz Dhandala
1ec25c27ee refactor: replace TouchableOpacity with Pressable for improved touch handling across components 2026-02-17 14:55:36 +00:00
Simon Larsen
5286527155 Merge pull request #2305 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-17 09:49:52 +00:00
simlarsen
895af10755 chore: npm audit fix 2026-02-17 02:28:09 +00:00
Nawaz Dhandala
77ccca7e2a refactor: simplify query client setup and improve deep link handling in navigation 2026-02-16 22:31:13 +00:00
Nawaz Dhandala
66f46e9b84 chore: remove unused whois-json type definitions and update package dependencies 2026-02-16 20:06:11 +00:00
Nawaz Dhandala
91edae50b2 feat: Implement domain monitoring criteria and secret handling for domain names 2026-02-16 19:17:00 +00:00
Nawaz Dhandala
7ab3dfe043 feat: Add Domain Monitor functionality with WHOIS integration
- Updated package.json to include whois-json dependency.
- Created DomainMonitorCriteria class for evaluating domain monitoring criteria.
- Added DomainMonitorResponse interface to define the structure of domain monitoring responses.
- Introduced MonitorStepDomainMonitor interface and utility for managing domain monitor steps.
- Developed DomainMonitorStepForm component for user input on domain monitoring settings.
- Implemented DomainMonitorView component to display monitoring results and details.
- Added DomainMonitorUtil class for querying domain information using WHOIS data.
- Included parsing methods for name servers and domain status in DomainMonitorUtil.
2026-02-16 19:10:44 +00:00
Nawaz Dhandala
fb661126d4 chore: bump version to 9.5.13 2026-02-16 17:10:17 +00:00
Nawaz Dhandala
94c57f3189 style: update DNSSEC comment to use block comment format for clarity 2026-02-16 17:10:06 +00:00
Nawaz Dhandala
4de6021905 fix: update DNSSEC check to use a default resolver if none specified 2026-02-16 17:09:41 +00:00
Nawaz Dhandala
c62a49d499 fix: combine iputils and dnsutils installation in Dockerfile 2026-02-16 16:56:11 +00:00
snyk-bot
01fd5263ca fix: upgrade playwright from 1.57.0 to 1.58.0
Snyk has created this PR to upgrade playwright from 1.57.0 to 1.58.0.

See this package in npm:
playwright

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/49c81d9c-12c2-4e8e-b9e8-72f98b1b595c?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-02-16 15:58:33 +00:00
Nawaz Dhandala
d87eee68e8 style: refactor arrow functions to use explicit return statements for clarity 2026-02-16 14:55:08 +00:00
Nawaz Dhandala
3f4db5b7e0 feat: implement project creation restriction for non-admin users 2026-02-16 14:54:14 +00:00
Nawaz Dhandala
462ad9d6ab Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-16 14:36:46 +00:00
Nawaz Dhandala
6444d3d5cc Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-16 14:36:31 +00:00
Nawaz Dhandala
415222561b feat: add health checks and common dependencies for services in Docker Compose files 2026-02-16 14:34:26 +00:00
Nawaz Dhandala
8cf2661c63 style: adjust spacing and styling for CardSelect option groups and EvaluationLogList component 2026-02-16 11:23:21 +00:00
Nawaz Dhandala
a820f817ff fix: update header color for option groups in CardSelect component 2026-02-16 11:20:14 +00:00
Nawaz Dhandala
576927c6c7 refactor: rename monitoring category from "Active Monitoring" to "Basic Monitoring" 2026-02-16 11:08:57 +00:00
Nawaz Dhandala
e866db9e18 feat: enhance monitor type handling with categorized card select options and improve DNS resolver configuration 2026-02-16 11:08:31 +00:00
Nawaz Dhandala
8e91a786f9 refactor: streamline route definition for CLI endpoint 2026-02-16 10:39:43 +00:00
Nawaz Dhandala
02d16446f1 feat: update documentation links in CLI and MCP server pages for improved navigation 2026-02-16 10:32:28 +00:00
Nawaz Dhandala
5d5517258b Refactor code structure for improved readability and maintainability 2026-02-16 08:59:39 +00:00
Nawaz Dhandala
5df632c46c feat: update CLI documentation to reflect changes in incident state handling and queries 2026-02-16 08:47:01 +00:00
Nawaz Dhandala
c1ee79b339 feat: enhance CLI documentation with additional context and options for commands 2026-02-16 08:41:48 +00:00
Nawaz Dhandala
67265c0fc8 feat: include createdAt and planName in project details for improved project information 2026-02-16 08:32:46 +00:00
Nawaz Dhandala
72e5384012 chore: remove ora dependency from package.json and package-lock.json 2026-02-16 08:30:39 +00:00
Nawaz Dhandala
dc8e9d44b1 chore: bump version to 9.5.12 2026-02-16 08:21:14 +00:00
Nawaz Dhandala
91102ee952 feat: add CLI section to navigation with links to documentation 2026-02-15 20:29:36 +00:00
Nawaz Dhandala
e46d1ae7da chore: update compile workflow to install and compile Common package 2026-02-15 20:19:08 +00:00
Nawaz Dhandala
008005415a fix: update MarkdownContent styles to use ReturnType for better type inference 2026-02-15 20:14:56 +00:00
Nawaz Dhandala
c7362f3ada feat: add comprehensive CLI documentation including authentication, command reference, resource operations, output formats, and scripting for automation 2026-02-15 20:10:21 +00:00
Nawaz Dhandala
1f634576fe chore: update CLI to use npm package for Common, adjust TypeScript config, and add CI workflow
- Changed dependency for Common in CLI package.json to use npm package @oneuptime/common@latest.
- Updated tsconfig.json to exclude Tests, build, node_modules, and jest.config.json.
- Modified PublishAllPackages.sh to replace Common dependency with the pinned version during publish.
- Added GitHub Actions workflow for testing CLI on pull requests and pushes.
2026-02-15 12:05:35 +00:00
Nawaz Dhandala
d25a97fe17 Refactor components for improved readability and consistency
- Added missing newlines at the end of files in MarkdownContent.tsx and RootCauseCard.tsx
- Reformatted shadowColor and color properties in NotesSection.tsx, SegmentedControl.tsx, MainTabNavigator.tsx, HomeScreen.tsx for better readability
- Enhanced code formatting in SectionHeader.tsx and OnCallStackNavigator.tsx for consistency
- Improved readability of getEntityId function in useAllProjectOnCallPolicies.ts
- Refactored conditional rendering in AlertDetailScreen.tsx, AlertEpisodeDetailScreen.tsx, IncidentDetailScreen.tsx, and IncidentEpisodeDetailScreen.tsx for better clarity
2026-02-15 11:47:32 +00:00
Nawaz Dhandala
b89ff11db8 Add comprehensive tests for CLI commands and error handling
- Implement tests for ResourceCommands, ConfigCommands, UtilityCommands, and ErrorHandler.
- Enhance test coverage for command registration and execution, including list, get, create, update, delete, and count operations.
- Introduce tests for credential management and context handling in commands.
- Add error handling tests to ensure graceful exits on API errors and invalid inputs.
- Update jest configuration to exclude test files from coverage and adjust TypeScript settings.
2026-02-15 10:54:50 +00:00
Nawaz Dhandala
5ac5ffede5 feat(cli): initialize CLI package with TypeScript configuration and dependencies
- Added package.json for OneUptime CLI with scripts for development and build processes.
- Included TypeScript configuration (tsconfig.json) with strict type checking and module settings.
2026-02-15 10:36:30 +00:00
Nawaz Dhandala
d9167b89ba feat: Add toPlainText utility function and update components to use it for improved text handling 2026-02-14 22:02:21 +00:00
Nawaz Dhandala
66b995c64a feat: Implement On-Call policies feature with navigation, API integration, and UI components 2026-02-14 21:51:09 +00:00
Nawaz Dhandala
f383bbba4d refactor: Update notification icons in MainTabNavigator for improved clarity 2026-02-14 21:42:19 +00:00
Nawaz Dhandala
43f1a59042 refactor: Update icon names in HomeScreen for improved clarity and consistency 2026-02-14 21:41:10 +00:00
Nawaz Dhandala
7d49872edc refactor: Enhance HomeScreen layout by adding section headers for Incidents and Alerts, improving organization and readability 2026-02-14 21:38:03 +00:00
Nawaz Dhandala
6d2d5892b9 refactor: Update label from 'Total active issues' to 'Total active items' and adjust total count calculation in HomeScreen for improved accuracy 2026-02-14 21:35:53 +00:00
Nawaz Dhandala
756217e19e refactor: Simplify layout in HomeScreen by removing unnecessary Live indicator for cleaner UI 2026-02-14 21:34:57 +00:00
Nawaz Dhandala
ca3cf01be7 refactor: Update icon for Root Cause section in AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen for improved clarity 2026-02-14 21:33:33 +00:00
Nawaz Dhandala
fd0a81a0b1 refactor: Remove unused Ionicons import and clean up RootCauseCard layout for improved readability 2026-02-14 21:31:49 +00:00
Nawaz Dhandala
14016d188b refactor: Replace MarkdownContent with RootCauseCard component in AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen for improved root cause display 2026-02-14 21:31:30 +00:00
Nawaz Dhandala
3a2aff7f34 refactor: Integrate MarkdownContent component for improved markdown rendering in FeedTimeline, AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen; update package.json and package-lock.json for new dependencies 2026-02-14 21:28:04 +00:00
Nawaz Dhandala
4a6edfee06 refactor: Update AlertCard, EpisodeCard, and IncidentCard components for improved styling and consistency; enhance rgbToHex function for better color handling 2026-02-14 21:23:20 +00:00
Nawaz Dhandala
2dc1b8aa8c refactor: Enhance footer in SettingsScreen with updated layout, styling, and open source acknowledgment 2026-02-14 21:18:45 +00:00
Nawaz Dhandala
eb0f0e742d refactor: Update color handling in GradientButton, NotesSection, SegmentedControl, and SettingsScreen for improved theme support 2026-02-14 21:16:37 +00:00
Nawaz Dhandala
23c82c5239 refactor: Update UI components for improved styling and consistency across AlertCard, EpisodeCard, IncidentCard, GradientButton, NotesSection, SegmentedControl, and detail screens 2026-02-14 21:15:32 +00:00
Nawaz Dhandala
2b61e4f4b7 refactor: Improve layout and accessibility in AlertCard, EpisodeCard, and IncidentCard components 2026-02-14 21:11:58 +00:00
Nawaz Dhandala
9b21abf78d refactor: Enhance footer in SettingsScreen with gradient background and open source acknowledgment 2026-02-14 20:56:02 +00:00
Nawaz Dhandala
bd54b38a69 refactor: Update footer text in SettingsScreen for clarity and add text alignment 2026-02-14 20:55:26 +00:00
Nawaz Dhandala
4dc799d238 refactor: Add useWindowDimensions for responsive tab bar label visibility 2026-02-14 20:52:24 +00:00
Nawaz Dhandala
b4d90e3bef refactor: Update color palette to neutral monochrome accents for improved consistency 2026-02-14 20:51:14 +00:00
Nawaz Dhandala
6c8d4203da refactor: Update Logo component sizes in HomeScreen and SettingsScreen for improved layout 2026-02-14 20:50:19 +00:00
Nawaz Dhandala
f7e9745624 refactor: Update Logo component to use SvgXml and adjust sizes in HomeScreen and SettingsScreen 2026-02-14 20:48:00 +00:00
Nawaz Dhandala
f7d133adba refactor: Add TypeScript types for alert and incident models and update tsconfig paths 2026-02-14 20:38:15 +00:00
Nawaz Dhandala
b06c2cb1c3 refactor: Add root cause field to alert and incident APIs and update detail screens for display 2026-02-14 20:31:06 +00:00
Nawaz Dhandala
b51c5d9677 refactor: Improve UI components with enhanced styles and layout adjustments across multiple screens 2026-02-14 20:29:12 +00:00
Nawaz Dhandala
9a1e265d1c refactor: Enhance UI with LinearGradient backgrounds and improve component styles across multiple screens 2026-02-14 20:26:05 +00:00
Nawaz Dhandala
e18d75fc8e refactor: Add TypeScript declarations for additional languages in react-syntax-highlighter across multiple files 2026-02-14 20:14:11 +00:00
Nawaz Dhandala
5a68d2f726 refactor: Add TypeScript declarations for additional languages in react-syntax-highlighter 2026-02-14 20:03:04 +00:00
Nawaz Dhandala
dfa7c4875a refactor: Simplify JSX structure in multiple components for improved readability 2026-02-14 20:00:20 +00:00
Nawaz Dhandala
8a568e0495 chore: Bump version to 9.5.11 2026-02-14 19:59:46 +00:00
Nawaz Dhandala
7152058ee2 Merge branch 'dash-chunk' 2026-02-14 19:54:45 +00:00
Nawaz Dhandala
b198dc0ec8 refactor: Enhance lazy loading of components in StatusPage for improved type safety and maintainability 2026-02-14 19:54:05 +00:00
Nawaz Dhandala
b0a3f8d60f refactor: Enable tree shaking in esbuild configuration for optimized bundle size 2026-02-14 19:42:53 +00:00
Nawaz Dhandala
83a13635cf Refactor route components to improve readability and consistency in prop spreading 2026-02-14 18:38:45 +00:00
Nawaz Dhandala
a2c1744e8c refactor: Enhance mermaid plugin to serve pre-bundled CJS file with ESM export for improved compatibility 2026-02-14 15:00:28 +00:00
snyk-bot
9dfbc05618 fix: Nginx/Dockerfile.tpl to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE322-LIBPNG-15062355
- https://snyk.io/vuln/SNYK-ALPINE322-LIBPNG-15062356
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121112
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121113
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121196
2026-02-14 14:53:26 +00:00
Nawaz Dhandala
4271ddbdcb refactor: Consolidate lazy loading of components into AllPages for improved organization and performance 2026-02-14 14:52:55 +00:00
Nawaz Dhandala
6ffb081a02 refactor: Consolidate route imports into AllRoutes for improved organization and maintainability 2026-02-14 14:51:01 +00:00
Nawaz Dhandala
16a9edbfcd Refactor route imports in StatusPagesRoutes, TracesRoutes, UserSettingsRoutes, and WorkflowRoutes for improved readability and performance by replacing lazy loading with direct imports. Remove unnecessary Suspense components around routes. 2026-02-14 11:00:15 +00:00
Nawaz Dhandala
e32d4395a3 refactor: update UI components for consistency and improved theming
- Refactored IncidentsScreen to use theme colors for backgrounds and text.
- Adjusted font sizes and styles across various components for better readability.
- Updated SettingsScreen to enhance layout and visual hierarchy, including removing GlassCard and using View components with theme colors.
- Modified LoginScreen and ServerUrlScreen to improve input field styling and overall layout.
- Revised color tokens in theme/colors.ts for better contrast and accessibility.
- Improved button labels for clarity and consistency.
2026-02-13 21:24:31 +00:00
Nawaz Dhandala
b8cd3ce1c1 Merge branch 'release' of https://github.com/OneUptime/oneuptime into release 2026-02-13 20:57:59 +00:00
Nawaz Dhandala
b86aee7f2a refactor: Update comments for channel name normalization and sanitization in Slack and Teams utilities 2026-02-13 20:57:25 +00:00
Nawaz Dhandala
2cde167445 refactor: Enhance channel name normalization to remove invalid characters for Microsoft Teams 2026-02-13 20:56:22 +00:00
Nawaz Dhandala
9bd6b011fe refactor: Sanitize Slack channel names to remove invalid characters 2026-02-13 20:52:02 +00:00
Nawaz Dhandala
538eef5660 chore: Bump version to 9.5.10 2026-02-13 19:27:29 +00:00
Simon Larsen
e1a343ae38 Merge pull request #2300 from OneUptime/master
Release
2026-02-13 19:27:00 +00:00
Nawaz Dhandala
8b42af35c1 refactor: Remove unused noteModalVisible prop from NotesSection component 2026-02-13 17:12:49 +00:00
Nawaz Dhandala
fc9026a8d8 refactor: Clean up component code and improve formatting across multiple files 2026-02-13 17:11:00 +00:00
Nawaz Dhandala
86edee35c1 style: Update color scheme and improve UI consistency across components 2026-02-13 17:09:12 +00:00
Nawaz Dhandala
109c276bc5 feat: Update GlassCard component to support opaque prop and apply it to multiple cards 2026-02-13 16:55:15 +00:00
Nawaz Dhandala
8040dd0f56 Refactor UI components and enhance styling
- Removed unused theme variable from IncidentsScreen.
- Updated SettingsScreen to use GlassCard and LinearGradient for improved UI.
- Refactored LoginScreen and ServerUrlScreen to utilize GradientHeader and GlassCard for consistent styling.
- Introduced GradientButton component for reusable gradient-styled buttons.
- Added Logo component for consistent branding across screens.
- Created NotesSection component to manage and display internal notes.
- Implemented SectionHeader component for better organization of section titles.
- Enhanced color theme with new gradient and accent colors.
2026-02-13 16:45:48 +00:00
Nawaz Dhandala
00d4148b6b Enhance UI and UX across multiple screens
- Updated BiometricLockScreen with improved icon layout and shadow effects.
- Refined HomeScreen layout for better spacing and added header gradient.
- Enhanced IncidentDetailScreen with gradient backgrounds and improved badge styling.
- Replaced error handling in IncidentsScreen with a reusable EmptyState component.
- Improved SettingsScreen with a profile header and added icons to settings rows.
- Revamped LoginScreen and ServerUrlScreen with gradient headers and refined input fields.
- Updated color theme to include new gradient properties for better visual consistency.
2026-02-13 16:14:15 +00:00
Nawaz Dhandala
dec03bc3a8 feat: Add expo-font dependency to package.json and package-lock.json 2026-02-13 15:45:30 +00:00
Nawaz Dhandala
46a9f95fc0 fix: Update error handling in getUserMiddleware to use NotAuthenticatedException 2026-02-13 15:42:10 +00:00
Simon Larsen
8b2f9bc778 Merge pull request #2299 from OneUptime/dna-monitor
DNS monitor
2026-02-13 14:04:51 +00:00
Nawaz Dhandala
fcc6223850 refactor: Remove unused variables and improve type annotations in hooks and screens 2026-02-13 14:04:23 +00:00
Nawaz Dhandala
c9bc214e86 fix: Correct CAA record resolution method and typo in value formatting 2026-02-13 13:57:31 +00:00
Simon Larsen
2897a937ba Merge pull request #2298 from OneUptime/refactor-mob-app
Refactor mob app
2026-02-13 13:49:05 +00:00
Nawaz Dhandala
f3cd7be143 refactor: Clean up code formatting and improve readability across multiple components 2026-02-13 13:48:46 +00:00
Nawaz Dhandala
f324a4e864 feat: Implement DNS monitoring configuration and secret handling 2026-02-13 13:42:45 +00:00
Nawaz Dhandala
f6a8cef649 feat: Add DNS monitoring capabilities
- Introduced new DNS monitor types and criteria checks in CriteriaFilter.ts.
- Implemented DNS monitor instance creation in MonitorCriteriaInstance.ts.
- Enhanced MonitorStep to support DNS configurations.
- Added DNS response handling in ProbeMonitorResponse.ts.
- Created DNS monitor forms and summary views in the Dashboard.
- Developed DNS query utilities and response handling in MonitorTypes/DnsMonitor.ts.
- Added DNS record types and response structures for better data handling.
- Implemented criteria evaluation for DNS monitoring in DnsMonitorCriteria.ts.
2026-02-13 13:29:01 +00:00
Nawaz Dhandala
770ef007a4 refactor: Enhance response normalization in API client to handle serialized types 2026-02-13 12:10:42 +00:00
Nawaz Dhandala
dafa0cc5d9 refactor: Update styling in AppContent and RootNavigator for consistent theme integration 2026-02-13 11:48:31 +00:00
Nawaz Dhandala
196e9cae10 refactor: Enhance error handling in getBlogPost method to return null on failure 2026-02-13 11:33:02 +00:00
Nawaz Dhandala
d0d26d20b2 refactor: Integrate SplashScreen to hide native splash screen after loading completes 2026-02-13 11:25:13 +00:00
Nawaz Dhandala
6a90ee97bf Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-13 11:12:22 +00:00
Nawaz Dhandala
f2a2644b0e Merge branch 'release' 2026-02-13 11:12:12 +00:00
Nawaz Dhandala
5cb2ac8c8b fix: Use absolute path for npm in ts-node installation to ensure correct execution 2026-02-13 11:10:34 +00:00
Nawaz Dhandala
6751d59b2f refactor: Remove totalActive calculation and active issues summary from HomeScreen 2026-02-13 11:05:09 +00:00
Nawaz Dhandala
aefc649743 refactor: Integrate useAuth in ProjectProvider for authentication handling 2026-02-13 10:25:57 +00:00
Nawaz Dhandala
cfba73665c refactor: Update loading state to use 'isPending' instead of 'isLoading' in multiple hooks 2026-02-13 10:21:06 +00:00
Nawaz Dhandala
049c5d003c refactor: Add flex styling to SectionList in AlertsScreen and IncidentsScreen for better layout 2026-02-13 10:15:17 +00:00
Simon Larsen
fd8998952d Merge pull request #2297 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-13 10:06:30 +00:00
Nawaz Dhandala
5ca85e4915 refactor: Update header comment in fetchProjects function for clarity on multi-tenant request 2026-02-13 09:58:09 +00:00
Nawaz Dhandala
aa401291b6 refactor: Update SettingsRow component to consolidate version display in SettingsScreen 2026-02-13 09:01:30 +00:00
Nawaz Dhandala
bf3d90871d refactor: Add feed fetching functions and integrate FeedTimeline component in detail screens 2026-02-13 09:00:20 +00:00
Nawaz Dhandala
4a4dff9264 refactor: Add 'muted' prop to AlertCard, EpisodeCard, and IncidentCard; implement opacity adjustment based on prop value 2026-02-13 08:51:36 +00:00
Nawaz Dhandala
fd3f75e4e2 refactor: Consolidate API fetching logic for alerts, incidents, and episodes; streamline hooks to use unified fetching methods 2026-02-13 08:46:59 +00:00
Nawaz Dhandala
43fc5acdda Refactor HomeScreen to consolidate project counts using useAllProjectCounts hook; remove unused hooks and simplify loading logic
Remove ProjectSelectionScreen as it is no longer needed; integrate project selection into other screens

Update IncidentDetailScreen and IncidentEpisodeDetailScreen to directly receive projectId from route params

Refactor IncidentsScreen to utilize useAllProjectIncidents, useAllProjectIncidentEpisodes, and useAllProjectIncidentStates hooks for better data management

Add new hooks: useAllProjectCounts, useAllProjectAlerts, useAllProjectAlertEpisodes, useAllProjectAlertStates, useAllProjectIncidentEpisodes, and useAllProjectIncidentStates for improved data fetching and state management

Remove useProject hook usage from SettingsScreen and adjust project-related logic accordingly
2026-02-13 08:39:24 +00:00
simlarsen
c7ca6138f3 chore: npm audit fix 2026-02-13 02:34:23 +00:00
Nawaz Dhandala
87475b00c4 Enhance UI components and improve accessibility across multiple screens
- Added SectionHeader component for consistent section titles in IncidentEpisodeDetailScreen.
- Updated styles for various text elements to improve readability and accessibility.
- Refined button styles in LoginScreen and ServerUrlScreen for better visual feedback.
- Introduced focus states for input fields in LoginScreen and ServerUrlScreen.
- Enhanced ProjectSelectionScreen with improved project item display.
- Implemented SectionCard component in SettingsScreen for better layout organization.
- Updated color tokens in theme for better visual consistency and added new accent colors.
- Adjusted Tailwind CSS configuration for improved shadow effects and new color variables.
2026-02-12 22:36:25 +00:00
Nawaz Dhandala
d5613cc4bd refactor: Integrate Ionicons for improved iconography in EmptyState, BiometricLockScreen, HomeScreen, and SettingsScreen 2026-02-12 22:19:24 +00:00
Nawaz Dhandala
b1c9d9a645 refactor: Add Ionicons for tab bar icons in MainTabNavigator 2026-02-12 22:17:33 +00:00
Nawaz Dhandala
01c6101ae9 Refactor navigation and screens for incidents and alerts
- Removed AlertEpisodesStackNavigator and IncidentEpisodesStackNavigator.
- Integrated AlertEpisodeDetailScreen and IncidentEpisodeDetailScreen into AlertsStackNavigator and IncidentsStackNavigator respectively.
- Updated MainTabNavigator to remove references to the deleted stack navigators.
- Adjusted RootNavigator linking to reflect the new navigation structure.
- Modified types to remove unused stack parameter lists.
- Updated handlers for notifications to navigate to the correct screens.
- Refactored AlertsScreen and IncidentsScreen to support segmented control for toggling between alerts and episodes.
- Added SegmentedControl component for better UI navigation.
- Cleaned up unused screen components and hooks related to episodes.
2026-02-12 22:14:55 +00:00
Nawaz Dhandala
ec56609bf4 refactor: Update biometric labels for clarity in SettingsScreen 2026-02-12 22:07:44 +00:00
Nawaz Dhandala
e5f652a950 refactor: Remove Notification Preferences screen and related navigation; update settings stack and types accordingly 2026-02-12 22:06:36 +00:00
Nawaz Dhandala
749bd2e41d refactor: Replace react-native-keychain with AsyncStorage for token management 2026-02-12 22:03:50 +00:00
Nawaz Dhandala
cc23416ad8 refactor: Move SafeAreaProvider to wrap the PersistQueryClientProvider for improved layout handling 2026-02-12 21:56:58 +00:00
Nawaz Dhandala
86fda9ba16 refactor: Update Tailwind CSS configuration and clean up imports; remove unused tailwind.config.ts file 2026-02-12 21:54:17 +00:00
Nawaz Dhandala
969983043b feat: Add babel-preset-expo to dependencies for improved Babel configuration 2026-02-12 21:39:30 +00:00
Nawaz Dhandala
2b64dd0b1d Refactor Settings, Login, and Server URL screens to use Tailwind CSS for styling; remove unused styles and theme properties; integrate NativeWind for utility-first styling approach; update theme context to support dark mode; enhance accessibility and maintainability. 2026-02-12 21:31:38 +00:00
Nawaz Dhandala
3a514969dc Refactor UI components to enhance styling and shadows
- Updated background colors for various components to use `backgroundElevated` instead of `backgroundSecondary`.
- Increased padding and border radius for cards and buttons across multiple screens for a more modern look.
- Introduced shadow styles for components to improve depth perception.
- Adjusted header styles in navigators to use `backgroundPrimary` and added subtle borders.
- Added a new `shadows` utility to manage shadow styles consistently across the app.
- Modified text styles for better readability and visual hierarchy.
- Updated spacing constants to include larger values for improved layout.
2026-02-12 21:02:46 +00:00
Nawaz Dhandala
10d006890c Refactor code structure for improved readability and maintainability 2026-02-12 20:46:28 +00:00
Nawaz Dhandala
2cb719d53a feat: Add check to skip localhost instances in OpenSourceDeploymentAPI 2026-02-12 20:34:30 +00:00
Nawaz Dhandala
eafb543371 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-12 19:35:57 +00:00
Nawaz Dhandala
5800fe4f7a feat: Implement aggressive disk cleanup in release workflow to optimize build space 2026-02-12 18:55:47 +00:00
Nawaz Dhandala
8d3712c36a feat: Enhance probe handling in secrets.yaml with improved logic for existing secrets 2026-02-12 17:05:33 +00:00
Nawaz Dhandala
f8e26246dd refactor: Improve related types handling and add peer dependencies in package-lock.json 2026-02-12 10:33:04 +00:00
Nawaz Dhandala
8560ecab41 chore: Bump version to 9.5.9 2026-02-12 09:14:53 +00:00
Nawaz Dhandala
5b2a6924d9 feat: Enhance DataTypeDetail and data-type.ejs with enriched related types and summary box 2026-02-11 22:51:39 +00:00
Nawaz Dhandala
e047143974 refactor: Improve type annotations and code readability in Dropdown component and migration file 2026-02-11 22:43:09 +00:00
Nawaz Dhandala
d23dc791e2 feat: Enhance data type documentation with category grouping and related types links 2026-02-11 22:38:41 +00:00
Nawaz Dhandala
a4b3b340c8 Add new data types for monitoring criteria and configurations
- Introduced CriteriaFilter, CriteriaIncident, and CriteriaAlert for defining filter conditions and incident configurations.
- Added enums for CheckOn, FilterType, and FilterCondition to specify evaluation aspects and comparison operators.
- Included configurations for various monitor step types: Log, Trace, Metric, and SNMP monitors.
2026-02-11 22:25:59 +00:00
Nawaz Dhandala
2173e4e611 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-11 22:10:58 +00:00
Nawaz Dhandala
dc19f87404 refactor: Clean up code formatting and improve readability across multiple files 2026-02-11 22:08:17 +00:00
Nawaz Dhandala
a3045c5f26 feat: Implement PermissionPicker component for enhanced permission selection 2026-02-11 22:04:25 +00:00
Nawaz Dhandala
5d6907be97 feat: Enhance dropdown options handling by introducing DropdownOptionGroup support 2026-02-11 21:42:57 +00:00
Nawaz Dhandala
e2ace9fc11 feat: Enhance permissions documentation with quick navigation and category links 2026-02-11 21:18:43 +00:00
Nawaz Dhandala
5a11bf228a feat: Enhance permissions handling by grouping permissions into categories 2026-02-11 21:14:55 +00:00
Nawaz Dhandala
cdd8d5523f Implement feature X to enhance user experience and optimize performance 2026-02-11 21:06:04 +00:00
Nawaz Dhandala
f5029fada7 feat: Refactor TypeToDocPath mapping to use dynamic generation from DataTypes 2026-02-11 20:54:47 +00:00
Nawaz Dhandala
8131c9d42f feat: Swap Data Types and Resources sections in navigation 2026-02-11 20:47:15 +00:00
Nawaz Dhandala
946c7d4c48 feat: Add detailed data type documentation and navigation
- Implemented a new DataTypeDetail service to handle detailed views for data types.
- Created a DataTypeUtil to manage data type definitions and retrieval.
- Added a new EJS template for displaying data type details, including properties, values, and JSON examples.
- Updated navigation to include a section for data types in both desktop and mobile views.
- Introduced a new DataTypes module to encapsulate data type information and improve maintainability.
2026-02-11 20:46:02 +00:00
Simon Larsen
9cbc7d9646 Merge pull request #2293 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-11 20:20:52 +00:00
Nawaz Dhandala
72d95871f7 feat: enhance model documentation links in API reference view 2026-02-11 19:24:51 +00:00
Nawaz Dhandala
3545a221bc feat: Add Alert Creation Page and Update Related Components
- Implemented a new page for creating alerts with a form that includes fields for title, description, severity, state, monitor, on-call policy, labels, root cause, and remediation notes.
- Updated the AlertsTable component to conditionally render a "Create Alert" button based on the disableCreate prop.
- Modified the AlertsLayout to optionally hide the side menu when navigating to the create alert page.
- Enhanced routing to include a new route for the alert creation page and adjusted the side menu visibility accordingly.
- Updated PageMap and RouteMap to include the new alert creation route.
- Added migration to update database schema related to incident and alert policies.
2026-02-11 19:17:25 +00:00
Nawaz Dhandala
9a5bcb9f31 feat: add migration for updating constraints and indexes in Incident and Alert OnCallDutyPolicy tables 2026-02-11 18:25:10 +00:00
Nawaz Dhandala
91a4d3601c fix: rename columns and update constraints in AlertOnCallDutyPolicy and IncidentOnCallDutyPolicy migrations 2026-02-11 18:23:43 +00:00
Nawaz Dhandala
ea99dd4873 refactor: update model relationships and descriptions for On-Call Duty Policies in Alert, Incident, and IncidentTemplate models; add migration for database schema changes 2026-02-11 18:16:38 +00:00
Nawaz Dhandala
34863dbcb6 fix: rename "Deleted by User ID" to "Acknowledged by User ID" in OnCallDutyPolicyExecutionLog and UserOnCallLog models 2026-02-11 18:09:28 +00:00
Nawaz Dhandala
effeb3a0b6 fix: update title and description for Monitor ID in Alert model 2026-02-11 18:06:59 +00:00
Nawaz Dhandala
86bdcb416a fix: await token refresh before retrying original request in response interceptor 2026-02-11 17:38:36 +00:00
Nawaz Dhandala
665f194f6d feat: add peer dependency flag to multiple packages in package-lock.json 2026-02-11 15:01:14 +00:00
Nawaz Dhandala
1378445dc5 feat: update dependencies for React and React Native 2026-02-11 14:59:54 +00:00
Nawaz Dhandala
236be5b60e feat: conditionally include prefix properties in alert and incident services 2026-02-11 14:53:57 +00:00
Nawaz Dhandala
256f4334eb Merge branch 'release' 2026-02-11 14:32:22 +00:00
Nawaz Dhandala
28d5ad4292 chore: bump version to 9.5.8 2026-02-11 14:32:03 +00:00
Nawaz Dhandala
5c169ccd5b feat: simplify props handling in Slack alert and incident episode actions 2026-02-11 14:27:26 +00:00
Nawaz Dhandala
e05f15d3f6 feat: refactor episode state update methods for Slack and Microsoft Teams actions 2026-02-11 14:24:50 +00:00
Nawaz Dhandala
de0cbe1f42 feat: update on-call policy notifications in Slack and Teams actions for clarity 2026-02-11 13:36:58 +00:00
Nawaz Dhandala
fc48a0efdb feat: add notification for missing on-call policies in Slack actions 2026-02-11 13:31:02 +00:00
Nawaz Dhandala
e623c973ee feat: enhance incident episode note handling with public/private options 2026-02-11 13:26:00 +00:00
Nawaz Dhandala
8d56287892 feat: update workspace channel retrieval to support incident and alert episodes 2026-02-11 13:09:21 +00:00
Nawaz Dhandala
0950d4288f feat: add episode and incident number prefixes to relevant services and messages 2026-02-11 12:58:23 +00:00
Nawaz Dhandala
56ea1c4690 fix: streamline formatting in various services for consistency 2026-02-11 12:52:15 +00:00
Nawaz Dhandala
08d2b6f5a2 feat: add emoji to button titles in Slack incident episode messages 2026-02-11 12:49:21 +00:00
Nawaz Dhandala
2cabdde5bd feat: add workspace notification handling for alert and incident episode services 2026-02-11 12:45:16 +00:00
Nawaz Dhandala
3e48a706bd feat: enhance workspace notification handling in various services 2026-02-11 12:39:48 +00:00
Nawaz Dhandala
7c672e14a1 feat: add workspace notification handling in Alert and Incident episode services 2026-02-11 12:16:42 +00:00
Nawaz Dhandala
80a3bbac3d Merge branch 'master' into release 2026-02-11 11:46:59 +00:00
simlarsen
25f9b826cf chore: npm audit fix 2026-02-11 02:38:02 +00:00
Nawaz Dhandala
c478e6af30 feat: add openSourceDeployment schema with webhookUrl property 2026-02-10 23:26:06 +00:00
Nawaz Dhandala
555a722732 feat: update expo-device to version 8.0.10 and react-native-screens to version 4.16.0 2026-02-10 23:24:47 +00:00
Nawaz Dhandala
6b5f981424 feat: refactor push token handling and move related functions to pushTokenUtils 2026-02-10 23:23:17 +00:00
Nawaz Dhandala
e0e614cf21 feat: enable new architecture in app.json 2026-02-10 23:17:53 +00:00
Nawaz Dhandala
44cc072d98 feat: add expo-system-ui dependency to package.json and package-lock.json 2026-02-10 23:15:38 +00:00
Nawaz Dhandala
c2c97dae0a feat: add mobile app Android and iOS deployment jobs to release workflow 2026-02-10 23:09:02 +00:00
Nawaz Dhandala
3978374ccb feat: add release signing setup documentation for Android and iOS 2026-02-10 23:08:24 +00:00
Nawaz Dhandala
6950daf10a feat: add workflows for mobile app Android and iOS deployment 2026-02-10 23:06:32 +00:00
Nawaz Dhandala
7a07e669c9 feat: add mobile app compilation and testing workflows 2026-02-10 22:55:57 +00:00
Nawaz Dhandala
67ece0fcca refactor: enhance type safety and improve code readability across multiple files 2026-02-10 22:45:12 +00:00
Nawaz Dhandala
5413e24bd4 refactor: enhance type safety and improve code readability across multiple files
- Updated hooks to return specific types using UseQueryResult for better type safety.
- Refactored various components to explicitly define return types for functions and callbacks.
- Improved type annotations for variables and function parameters in screens and hooks.
- Enhanced readability by restructuring code and ensuring consistent formatting.
- Added missing type imports and ensured proper usage of types from the API.
- Cleaned up unnecessary type assertions and improved overall code clarity.
2026-02-10 22:38:45 +00:00
Nawaz Dhandala
59b3fc0334 Refactor screens and components for improved readability and consistency
- Simplified state management and data fetching in IncidentEpisodeDetailScreen.
- Enhanced code clarity by using arrow functions consistently and removing unnecessary destructuring.
- Improved type annotations across various screens for better TypeScript support.
- Streamlined the rendering of components in IncidentEpisodesScreen and IncidentsScreen.
- Updated NotificationPreferencesScreen to use consistent function signatures and improved readability.
- Refactored ProjectSelectionScreen and SettingsScreen for better structure and clarity.
- Enhanced LoginScreen and ServerUrlScreen with clearer type definitions and improved error handling.
- Updated storage utilities to ensure consistent type usage and improved code clarity.
- Refactored theme context and spacing utilities for better type safety and readability.
- Improved color and date utility functions for better maintainability.
2026-02-10 22:29:37 +00:00
Nawaz Dhandala
2bc72dbdb6 feat: update project view route to use current navigation context 2026-02-10 22:21:19 +00:00
Nawaz Dhandala
15ccf00503 chore: update expo-server-sdk to version 3.15.0; reorganize push notifications documentation
- Updated expo-server-sdk in package.json from 3.10.0 to 3.15.0.
- Deleted outdated firebase-push-notifications.md and created new push-notifications.md for clarity.
- Updated navigation links in Nav.ts and README.md to point to the new push-notifications documentation.
2026-02-10 22:07:39 +00:00
Nawaz Dhandala
b3d73a5523 feat: add notification preferences screen and settings stack navigator
feat: implement notification preferences management with local storage
feat: enhance accessibility for alert and incident actions
feat: integrate haptic feedback for user interactions in various screens
refactor: update navigation structure to include settings and notification preferences
2026-02-10 22:04:37 +00:00
Nawaz Dhandala
43e6291608 feat: enhance incident detail screens with haptic feedback and loading skeletons
- Added haptic feedback on state change actions in IncidentDetailScreen and IncidentEpisodeDetailScreen.
- Replaced ActivityIndicator with SkeletonCard for better loading experience in IncidentDetailScreen and IncidentEpisodeDetailScreen.
- Updated empty state messages in IncidentEpisodesScreen and IncidentsScreen for clarity.
- Refactored SettingsScreen to improve layout and added biometric authentication options.
- Introduced OfflineBanner component to notify users of network issues.
- Created SwipeableCard component for better interaction with list items.
- Implemented useBiometric and useNetworkStatus hooks for managing biometric settings and network status.
- Added BiometricLockScreen for unlocking the app using biometric authentication.
- Introduced preferences storage for theme mode and biometric settings.
2026-02-10 21:54:18 +00:00
Nawaz Dhandala
09d82f64de refactor: remove Firebase Cloud Messaging configuration and related code for push notifications 2026-02-10 21:38:42 +00:00
Nawaz Dhandala
51ed9fc2bb feat: implement push notification registration and unregistration functionality 2026-02-10 21:27:35 +00:00
Nawaz Dhandala
b23ccdcc57 feat: add Incident and Alert Episodes screens and navigators
- Created IncidentEpisodesStackNavigator for managing navigation between incident episodes list and detail screens.
- Implemented AlertEpisodesScreen to display a list of alert episodes with pagination and refresh functionality.
- Developed AlertEpisodeDetailScreen to show detailed information about a specific alert episode, including state changes and internal notes.
- Added IncidentEpisodesScreen to display a list of incident episodes with similar functionality to the alert episodes screen.
- Created IncidentEpisodeDetailScreen for detailed view of incident episodes, including state management and note-taking features.
- Integrated hooks for fetching data related to alert and incident episodes.
- Added UI components for displaying episode details, actions, and notes.
2026-02-10 20:34:53 +00:00
Nawaz Dhandala
147e687bac fix: refactor alert and incident state handling to use state IDs for acknowledgment and resolution 2026-02-10 20:15:46 +00:00
Nawaz Dhandala
b84cebcb10 feat: add hooks and screens for alerts and incidents management
- Implemented `useAlertDetail`, `useAlertStates`, and `useAlertStateTimeline` hooks for fetching alert details, states, and timelines.
- Created `useAlerts` and `useUnresolvedAlertCount` hooks for managing alerts list and unresolved alert counts.
- Developed `useIncidentDetail`, `useIncidentStates`, and `useIncidentStateTimeline` hooks for incident management.
- Added `useIncidents` and `useUnresolvedIncidentCount` hooks for fetching incidents and unresolved incident counts.
- Introduced `ProjectProvider` and `useProject` hook for managing project selection and state.
- Created `AlertsStackNavigator` and `IncidentsStackNavigator` for navigation between alerts and incidents screens.
- Developed `AlertDetailScreen` and `IncidentDetailScreen` for displaying detailed information about alerts and incidents.
- Added `ProjectSelectionScreen` for selecting projects with loading and error handling.
- Implemented utility functions for color conversion and date formatting.
2026-02-10 20:10:37 +00:00
Nawaz Dhandala
7374e3bf9a fix: update UI/UX design philosophy to emphasize native app experience 2026-02-10 18:00:54 +00:00
Nawaz Dhandala
413ba90b02 fix: correct property access for miscData in login function 2026-02-10 17:59:55 +00:00
Nawaz Dhandala
2fd61385bd fix: update login function to structure email and password as objects 2026-02-10 17:58:04 +00:00
Nawaz Dhandala
822bc9f8d5 fix: update newArchEnabled to false and modify start scripts in package.json 2026-02-10 17:54:43 +00:00
Nawaz Dhandala
e53a490606 Merge branch 'master' into release 2026-02-10 16:39:21 +00:00
Nawaz Dhandala
cc53460e7a feat: add README.md for OneUptime Mobile App setup and configuration 2026-02-10 16:01:51 +00:00
Nawaz Dhandala
7d6e0488ba feat: add Firebase Cloud Messaging configuration for native push notifications 2026-02-10 15:58:12 +00:00
Nawaz Dhandala
385a0fb9e5 fix: change deviceType property type to PushDeviceType in UserPush model 2026-02-10 15:34:21 +00:00
Nawaz Dhandala
584b79f48c Merge branch 'master' into mob-phase-1 2026-02-10 15:26:41 +00:00
Nawaz Dhandala
92901b1647 feat: add openSourceDeployment schema and update values.yaml documentation 2026-02-10 15:23:15 +00:00
Nawaz Dhandala
bcbc4f6d99 chore: bump version to 9.5.7 2026-02-10 15:00:30 +00:00
Nawaz Dhandala
04dd1260ac style: improve code formatting for better readability in Register and BasicForm components 2026-02-10 15:00:08 +00:00
Nawaz Dhandala
882f9f6ae4 feat: add support for full row spanning in form fields and conditionally display notification checkbox based on billing status 2026-02-10 14:58:51 +00:00
Nawaz Dhandala
549dc3546b refactor: clean up code formatting and improve readability in Authentication and OpenSourceDeploymentAPI 2026-02-10 14:14:33 +00:00
Nawaz Dhandala
25edcf7d9b feat: add migration for OpenSourceDeployment table and update OnCallDutyPolicyScheduleLayer defaults 2026-02-10 14:13:07 +00:00
Nawaz Dhandala
46378fc3db Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-10 14:11:31 +00:00
Nawaz Dhandala
f9f5bff4ce feat: add Open Source Deployment webhook support and related configuration 2026-02-10 14:11:03 +00:00
Nawaz Dhandala
12b78249c5 feat: rename version field to oneuptimeVersion in OpenSourceDeployment model and API 2026-02-10 13:57:55 +00:00
Nawaz Dhandala
f8cbc3a551 feat: implement Open Source Deployment registration and related database schema 2026-02-10 13:50:56 +00:00
Simon Larsen
670b984cee Merge pull request #2288 from OneUptime/episode-resolve
Episode resolve
2026-02-10 13:29:17 +00:00
Nawaz Dhandala
e677e54ea9 feat: update migration and services to handle allIncidentsResolvedAt and allAlertsResolvedAt fields 2026-02-10 13:28:18 +00:00
Nawaz Dhandala
928a2589c2 feat: remove workspaceProjectAuthTokenId from WorkspaceNotificationRule in migration 2026-02-10 13:20:40 +00:00
Nawaz Dhandala
45f7a86888 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-10 13:12:42 +00:00
Nawaz Dhandala
0fa7848ab9 feat: add migration for allIncidentsResolvedAt and allAlertsResolvedAt fields with index creation 2026-02-10 13:12:10 +00:00
Nawaz Dhandala
ae6e49da8f feat: add allAlertsResolvedAt and allIncidentsResolvedAt fields for resolve delay calculations 2026-02-10 13:08:42 +00:00
Simon Larsen
e80e22b1fa Merge pull request #2285 from OneUptime/inc-episode
feat(AutoResolve): implement resolve delay logic based on incident gr…
2026-02-10 12:42:17 +00:00
Simon Larsen
2adefd1cee Merge pull request #2287 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-10 11:17:17 +00:00
simlarsen
36cbb3159a chore: npm audit fix 2026-02-10 02:40:36 +00:00
Nawaz Dhandala
54909116b9 mobile phase 1 2026-02-09 23:45:17 +00:00
Nawaz Dhandala
4582f6100a feat(MobileApp): enhance UI/UX design philosophy with modern aesthetics and user experience considerations 2026-02-09 22:49:00 +00:00
Nawaz Dhandala
49a01eca8c feat(MobileApp): add UI/UX design philosophy and core design principles to the design document 2026-02-09 22:47:15 +00:00
Nawaz Dhandala
349df0e181 feat(MobileApp): enhance multi-project support with project badges and filters in UI 2026-02-09 22:43:03 +00:00
Nawaz Dhandala
c52116bec1 feat(MobileApp): add design document for OneUptime On-Call mobile app 2026-02-09 22:32:55 +00:00
Nawaz Dhandala
098a18005f feat(ResolveInactiveEpisodes): implement inactivity timeout logic based on incident grouping rules 2026-02-09 22:20:42 +00:00
Nawaz Dhandala
6dbcd69ecd feat(AutoResolve): implement resolve delay logic based on incident grouping rules 2026-02-09 21:49:49 +00:00
Nawaz Dhandala
09a6827709 refactor(Service): streamline incident count template replacement 2026-02-09 21:16:52 +00:00
Nawaz Dhandala
dbb1fa6c18 feat(IncidentEpisodeService): enhance incident count update with dynamic title and description templates 2026-02-09 21:15:56 +00:00
Nawaz Dhandala
cd450bc3b6 feat(migrations): set default value of groupByMonitor to false for Alert and Incident grouping rules 2026-02-09 20:15:15 +00:00
Nawaz Dhandala
047195116d feat(migrations): optimize backfill queries to use MAX instead of COUNT for performance and accuracy 2026-02-09 19:27:12 +00:00
Nawaz Dhandala
564f21388b chore(VERSION): bump version to 9.5.6 2026-02-09 18:21:05 +00:00
Nawaz Dhandala
c69d7c949e feat(templates): reorder service and autoscaler definitions in Helm chart templates 2026-02-09 18:04:54 +00:00
Nawaz Dhandala
dd47b9c3a9 feat(e2e-tests): add CRUD and idempotency tests for oneuptime_file resource 2026-02-09 14:46:40 +00:00
Nawaz Dhandala
ce731cb489 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-09 14:41:34 +00:00
Nawaz Dhandala
f725fdd2d9 feat(ResourceGenerator): implement no-op methods for read, update, and delete operations 2026-02-09 14:41:32 +00:00
Simon Larsen
1aec570c83 Merge pull request #2280 from OneUptime/feat/readme-ai-copilot
docs: add AI Copilot section to README
2026-02-09 14:32:57 +00:00
Jamie Mallers
97b7e15ece docs: add AI Copilot section to README
Highlight the AI agent capabilities - auto-detection, root cause analysis,
and automated code fix PRs. This is our key differentiator and was missing
from the README.
2026-02-09 14:31:03 +00:00
Nawaz Dhandala
7cdac5fe66 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-09 14:28:11 +00:00
Nawaz Dhandala
4add175070 refactor(Monitor): improve code readability by consolidating API ping parameters
refactor(Migration): update comments for clarity and maintainability
fix(MarkdownEditor): adjust code block formatting for consistency in documentation
2026-02-09 14:28:09 +00:00
Nawaz Dhandala
711cfd2f6b feat(MonitorUtil): add tests for URL placeholder resolution and update method visibility 2026-02-09 14:21:05 +00:00
Nawaz Dhandala
6869ee670a fix(VMUtil): skip replacement if variable is not found in storageMap 2026-02-09 14:17:20 +00:00
Nawaz Dhandala
c4d978cc3b feat: add API and Website monitor documentation with dynamic URL placeholders 2026-02-09 14:11:02 +00:00
Simon Larsen
1dffc2fbbe Merge pull request #2279 from OneUptime/feat/ga4-signup-tracking
feat(analytics): add GA4 sign_up and page view tracking
2026-02-09 13:56:18 +00:00
Jamie Mallers
6b0756cd3a feat(analytics): add GA4 sign_up and page view tracking
- Fire sign_up event on successful registration (Accounts/Register.tsx)
- Fire page_view_pricing event on pricing page load
- Fire page_view_demo event on demo page load
- All events use dataLayer.push for GTM/GA4 compatibility
2026-02-09 13:54:44 +00:00
Simon Larsen
fbfa7747e0 Merge pull request #2278 from OneUptime/fluentbit
Fluentbit
2026-02-09 13:14:59 +00:00
Nawaz Dhandala
a7c38dcbf2 refactor(FluentLogsIngestService): improve code readability by formatting function arguments and object properties 2026-02-09 13:14:39 +00:00
Nawaz Dhandala
6b8dd9e8b5 feat(FluentLogsIngestService): enhance log ingestion with structured fields and attributes extraction
test(FluentLogsIngestService): add comprehensive tests for log normalization and attribute extraction
2026-02-09 13:05:52 +00:00
Nawaz Dhandala
c5e7429b3d chore(VERSION): bump version to 9.5.5 2026-02-09 11:54:17 +00:00
Nawaz Dhandala
13ccee4e69 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-09 11:53:31 +00:00
Nawaz Dhandala
f9c9ac5ef0 fix(Migration): optimize backfill queries for project counters using JOINs 2026-02-09 11:53:29 +00:00
Nawaz Dhandala
10654a0a04 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-09 11:07:56 +00:00
Nawaz Dhandala
0d900dca78 fix(StatusPageDomain): change subdomain requirement from required to optional 2026-02-09 11:07:24 +00:00
Nawaz Dhandala
bf5846e7f4 fix(ProductCompare): improve question formatting for clarity in FAQ section 2026-02-08 22:25:06 +00:00
Nawaz Dhandala
9fa48c7a25 chore(VERSION): bump version to 9.5.4 2026-02-08 22:02:16 +00:00
Simon Larsen
9efb070334 Merge pull request #2276 from OneUptime/fix/seo-better-uptime-enhanced
SEO: Add FAQ schema to compare pages + enhance Better Uptime content
2026-02-08 18:46:52 +00:00
Nawaz Dhandala
85e7dd1150 fix(sitemap): update comment format for clarity on blog tag sitemaps removal 2026-02-08 18:16:51 +00:00
Nawaz Dhandala
072f162b6e fix(Project): update create permissions to allow User role 2026-02-08 18:12:26 +00:00
Nawaz Dhandala
9e01b0b75a fix(playwright): install browsers to a fixed path for runtime accessibility 2026-02-08 17:56:59 +00:00
Nawaz Dhandala
3d7b98d1ee fix(deps): update net-snmp to version 3.26.1 2026-02-08 15:37:41 +00:00
Jamie Mallers
62f6900dd2 Merge origin/master - resolve conflict in product-compare.ejs
Keep both:
- Updated meta description from master
- FAQ schema for rich snippets from this branch
2026-02-08 15:20:49 +00:00
Jamie Mallers
51cf4a88bf SEO: Add FAQ schema + enhance Better Uptime comparison content
Changes:
1. Add JSON-LD FAQ schema to all compare pages for rich snippets
2. Expand Better Uptime FAQs from 4 to 8 questions
3. Add more detailed answers covering:
   - Better Uptime → Better Stack rebrand context
   - Self-hosting advantages
   - Detailed pricing comparison
   - Feature differences

Target: /compare/better-uptime at position 13.5 for 'better uptime' query
Expected: Rich snippet eligibility + better content relevance
2026-02-08 09:21:54 +00:00
Simon Larsen
0dfd38d263 Merge pull request #2275 from OneUptime/fix/seo-compare-page-titles
fix(seo): improve compare page title tags for better CTR
2026-02-07 20:44:53 +00:00
Simon Larsen
66424eee24 Merge pull request #2274 from OneUptime/fix/seo-meta-descriptions
fix(seo): unique meta descriptions for product pages
2026-02-07 20:44:14 +00:00
Simon Larsen
93adee4b16 Merge pull request #2273 from OneUptime/fix/seo-noindex-tag-pages
fix(seo): noindex tag pages and remove from sitemap
2026-02-07 20:42:57 +00:00
Jamie Mallers
d7efe2445c fix(seo): improve compare page title tags for better CTR
Changed title pattern from:
'OneUptime vs [Product]: Open-Source Alternative | 2026 Comparison'

To:
'[Product] Alternative - OneUptime | Open Source | 2026 Comparison'

Rationale:
- Puts competitor name first (matches search intent)
- 'Alternative' keyword prominent (what users search for)
- Shorter, more scannable

Also improved meta description to be more action-oriented with clear value props.

Targets: /compare/better-uptime (position 13.5, 803 impressions)
2026-02-07 17:01:18 +00:00
Jamie Mallers
1bf4c52518 fix(seo): unique meta descriptions for product pages
Multiple product pages had identical generic meta descriptions:
'OneUptime monitors websites, APIs, and servers...'

This hurt CTR because Google showed the same text for different pages.

Updated with unique, keyword-rich descriptions:
- /product/status-page: Focus on free, unlimited subscribers, Statuspage alternative
- /product/monitoring: Focus on global locations, alerts, Datadog alternative
- /product/incident-management: Focus on features, integrations
- /product/on-call: Focus on rotations, escalations, PagerDuty alternative
- /about: Focus on open source, GitHub stars, mission

Expected impact: Improved CTR from search results
2026-02-07 16:53:21 +00:00
Jamie Mallers
8348bf6897 fix(seo): noindex tag pages and remove from sitemap
Google Search Console shows 0/10,519 pages indexed. Root cause: 5,000+
thin tag pages are diluting site quality signals and consuming crawl budget.

Changes:
- Add noindex,follow meta tag to blog tag pages (ListByTag.ejs)
- Remove tag sitemaps from sitemap index (Sitemap.ts)

This tells Google to:
1. Stop trying to index tag pages (they're thin content)
2. Still follow links on those pages to discover real content
3. Focus crawl budget on valuable pages (blog posts, product pages)

Expected impact:
- Improved crawl budget efficiency
- Better quality signals for the domain
- Gradual improvement in indexing of valuable pages
2026-02-07 15:49:51 +00:00
Simon Larsen
7f2192206f Merge pull request #2272 from OneUptime/prefix-alert-number
Prefix alert number
2026-02-06 20:50:57 +00:00
Nawaz Dhandala
ddf7636965 Set default number prefixes for incident, alert, and maintenance numbers in ProjectService 2026-02-06 20:48:02 +00:00
Nawaz Dhandala
52514fbb7e Rename parameter onCreate to _onCreate in onCreateSuccess method for clarity 2026-02-06 20:40:21 +00:00
Nawaz Dhandala
2c3521561d Add type annotations to fix ESLint typedef errors for numberResult variables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 20:31:31 +00:00
Nawaz Dhandala
8d6ef5a277 Refactor alert and incident number display formatting
- Updated multiple components and worker jobs to improve the formatting of alert and incident numbers.
- Ensured consistent use of conditional rendering for alert and incident numbers with prefixes.
- Enhanced readability by using multiline return statements for JSX elements.
2026-02-06 20:27:35 +00:00
Nawaz Dhandala
726ae7ef98 Fix prefix display in UI tables and detail views by adding WithPrefix to selectMoreFields
ModelTable/CardModelDetail only fetch the first key from column field objects.
The WithPrefix fields must be in selectMoreFields to be included in the API query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 20:19:52 +00:00
Nawaz Dhandala
fde974d968 Add alertNumberWithPrefix support to AlertEpisodeMemberService and UserNotificationRuleService
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 20:08:08 +00:00
Nawaz Dhandala
eae5e026fa Add prefix support to monitor evaluation log incident/alert number display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 20:00:44 +00:00
Nawaz Dhandala
5b01743e74 Add alertNumberWithPrefix support to AlertOwners Worker notification files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:53:01 +00:00
Nawaz Dhandala
2cfc6a5e68 feat: Add migration for new prefix fields in Project, Incident, Alert, and ScheduledMaintenance tables 2026-02-06 19:46:40 +00:00
Nawaz Dhandala
0976df1bee feat: Add custom prefixes for alert, incident, and scheduled maintenance numbers
- Implemented functionality to allow users to set custom prefixes for alert numbers and alert episode numbers in the AlertMoreSettings component.
- Added similar functionality for incident numbers and incident episode numbers in the IncidentMoreSettings component.
- Introduced custom prefixes for scheduled maintenance numbers in the ScheduledMaintenanceMoreSettings component.
- Updated various notification jobs to utilize the new prefix settings for alert, incident, and scheduled maintenance numbers.
- Ensured backward compatibility by maintaining the default '#' prefix when no custom prefix is provided.
2026-02-06 19:43:11 +00:00
Nawaz Dhandala
caa59aea7e Refactor Analytics event tracking for consistent string quoting 2026-02-06 14:46:44 +00:00
Nawaz Dhandala
ab5e0ec3c4 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-06 14:43:21 +00:00
Nawaz Dhandala
f4eda526c5 Remove semaphore mutex implementation from AlertEpisodeService and IncidentEpisodeService for simplified episode creation 2026-02-06 14:43:19 +00:00
Nawaz Dhandala
19f347a826 Remove semaphore mutex implementation from AlertService, IncidentService, and ScheduledMaintenanceService for simplified alert and incident creation 2026-02-06 14:39:49 +00:00
Nawaz Dhandala
7eb84c2fb0 Refactor increment methods in ProjectService to use atomicIncrementColumnValueByOne for better code reuse 2026-02-06 14:32:30 +00:00
Nawaz Dhandala
a27f3953ab Add counters for incidents, alerts, and scheduled maintenance to Project model and implement corresponding increment methods 2026-02-06 14:30:16 +00:00
Simon Larsen
4ec162208b Merge pull request #2269 from OneUptime/fix/add-ga4-conversion-tracking
Add GA4 conversion tracking for demo bookings and CTA clicks
2026-02-06 00:00:01 +00:00
Jamie Mallers
13482b13d7 Add GA4 tracking to Analytics module for signup and all events
- All events captured via Analytics.capture() now also push to GA4 dataLayer
- This enables tracking signups (accounts/register) and other events in GA4
- Works alongside existing PostHog tracking
2026-02-05 23:52:33 +00:00
Jamie Mallers
69c0253862 Add GA4 conversion tracking for demo bookings and CTA clicks
- Add GA4 gtag event tracking alongside PostHog for demo bookings
- Add dataLayer push for GTM compatibility
- Add CTA click tracking for 'Get started' and 'Request demo' buttons
- Enable conversion funnel analysis in Google Analytics

This fixes the issue where GA4 Key Events showed 0 conversions
despite demos being booked (only tracked in PostHog previously).
2026-02-05 23:42:10 +00:00
Nawaz Dhandala
92d8b7b425 Add computed property to various database models 2026-02-05 15:23:06 +00:00
Nawaz Dhandala
0ef053dc3d Add computed property to schedule next event column in ScheduledMaintenanceTemplate 2026-02-05 13:52:34 +00:00
Nawaz Dhandala
fa0bd99bc8 Bump version to 9.5.3 2026-02-05 12:58:30 +00:00
Simon Larsen
68b6ca9fd3 Merge pull request #2267 from OneUptime/episode-status-page
Episode status page
2026-02-05 12:58:03 +00:00
Simon Larsen
cd9b711ee4 Merge pull request #2268 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-05 12:56:59 +00:00
Nawaz Dhandala
b05d1652e1 Implement logic to mark episode public notes and state timelines as Skipped for subscriber notifications 2026-02-05 12:52:55 +00:00
Nawaz Dhandala
d971573db0 Refactor code for improved readability and consistency
- Adjusted formatting in PublicNote.tsx for better alignment of imports and function parameters.
- Reformatted episode monitor extraction in Detail.tsx for clarity.
- Enhanced readability of episodes data parsing in List.tsx.
2026-02-05 12:48:13 +00:00
Nawaz Dhandala
5a11518c31 Add episode settings section to Alert Grouping Rules page 2026-02-05 12:47:09 +00:00
Nawaz Dhandala
ce4f41367b Add episode settings section to Incident Grouping Rules page 2026-02-05 12:44:37 +00:00
Nawaz Dhandala
f553726186 Refactor code structure for improved readability and maintainability 2026-02-05 12:32:56 +00:00
Nawaz Dhandala
1bd746b285 Update icon SVG path for improved rendering and clarity 2026-02-05 12:14:41 +00:00
Nawaz Dhandala
82558fda59 Add episode management features to Status Page
- Implement fetching and displaying active incident episodes on the Status Page.
- Introduce episode public notes and state timelines for enhanced episode details.
- Create EpisodeGroup type to structure episode-related data.
- Update Overview component to integrate episodes alongside incidents.
2026-02-05 12:11:41 +00:00
Nawaz Dhandala
3f1fe2bf1c Preserve monitor data in episodes from raw JSON and update serialization logic 2026-02-05 12:00:57 +00:00
Nawaz Dhandala
d00fa80e47 Add monitor mapping and resource filtering for episode events in Detail component 2026-02-05 11:40:37 +00:00
Nawaz Dhandala
dc4805c3b2 Add settings page for incident episodes with routing and UI integration 2026-02-05 11:27:50 +00:00
Nawaz Dhandala
642fb95209 Replace Toggle components with Checkbox in MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm 2026-02-05 11:19:06 +00:00
Nawaz Dhandala
01baf60b2e Add endpoint and logic for retrieving incident episode public note attachments 2026-02-05 10:52:03 +00:00
simlarsen
225679f5d3 chore: npm audit fix 2026-02-05 02:25:19 +00:00
Nawaz Dhandala
51f16e2213 Add public notes feature for incident episodes with routing and UI integration 2026-02-04 21:55:32 +00:00
Nawaz Dhandala
0af23bbacb Add migration for new StatusPage fields and update index 2026-02-04 20:36:14 +00:00
Nawaz Dhandala
7b446a853c Add migration for StatusPage enhancements and OnCallDutyPolicyScheduleLayer defaults 2026-02-04 20:35:43 +00:00
Nawaz Dhandala
df480577ab Clarify comments regarding incident visibility and episode display logic in StatusPageAPI 2026-02-04 20:33:42 +00:00
Nawaz Dhandala
c031cc2af3 Refactor incident query logic to clarify episode visibility criteria on status page 2026-02-04 20:31:26 +00:00
Nawaz Dhandala
ae17820d0d Add subscriber episode notification templates for incident updates 2026-02-04 20:28:37 +00:00
Nawaz Dhandala
f2f3900506 Refactor notification templates and job scripts to replace "Episode" with "Incident" for consistency across the application. 2026-02-04 20:26:49 +00:00
Nawaz Dhandala
4c8b92144c Refactor code for improved readability and consistency: adjust formatting, enhance type annotations, and streamline notification logging in various files. 2026-02-04 20:20:08 +00:00
Nawaz Dhandala
4c45e16f56 Refactor episode handling: remove EpisodeDetail component and update routing; enhance incident and episode event item retrieval 2026-02-04 20:13:25 +00:00
Nawaz Dhandala
53e39724e7 Add episode management features to status page: enable episode display, history, and labels 2026-02-04 20:09:01 +00:00
Nawaz Dhandala
849882d868 Add notification jobs for incident episode public notes and state timelines
- Implemented SendNotificationToSubscribers job for IncidentEpisodePublicNote to notify subscribers about new public notes added to episodes.
- Implemented SendNotificationToSubscribers job for IncidentEpisodeStateTimeline to notify subscribers about state changes of episodes.
- Both jobs include logic for fetching relevant episodes, monitors, and subscribers, and sending notifications via email, SMS, Slack, and Microsoft Teams.
- Added error handling and logging for better traceability of notification processes.
2026-02-04 19:11:44 +00:00
Nawaz Dhandala
e3f8af83e5 Add showIncidentOnStatusPage feature to CriteriaIncident and update forms 2026-02-04 18:30:06 +00:00
Simon Larsen
18a5559116 Merge pull request #2265 from OneUptime/compare-pages-seo-improvement
Improve compare page SEO: add 'open-source alternative' positioning
2026-02-03 15:20:18 +00:00
Jamie Mallers
3c3ecfc698 Enhance compare pages: stronger positioning and CTAs
- Badge: 'Open Source Alternative' with GitHub link (reinforces positioning)
- H1: 'The Open-Source [Competitor] Alternative' (SEO + clear value prop)
- CTA: 'Start free — no credit card' (removes friction)
- Added trust signal: 'Self-host for free or use our cloud. No vendor lock-in.'

These changes make the compare pages more conversion-focused.
2026-02-03 15:16:51 +00:00
Jamie Mallers
c22f7fec46 Improve compare page SEO: add 'open-source alternative' positioning
- Title: 'OneUptime vs [Competitor]: Open-Source Alternative | 2026 Comparison'
- Meta: Highlights open-source, alternative positioning, and all-in-one value prop
- Targets high-intent search terms: '[competitor] alternative', 'open source [competitor]'

Affects all /compare/* pages (Datadog, PagerDuty, New Relic, Statuspage, etc.)
2026-02-03 15:12:45 +00:00
Simon Larsen
75d473f6d7 Merge pull request #2264 from OneUptime/homepage-messaging-update
Update homepage messaging: lead with open-source positioning
2026-02-03 15:04:40 +00:00
Jamie Mallers
5d8f8e248e Update homepage messaging: lead with open-source positioning
- Title: 'OneUptime | The Open-Source Observability Platform'
- Meta: Focus on unified platform, self-hostable
- H1: 'The Open-Source Observability Platform'
- Sub: Emphasize complete reliability stack (monitoring, incidents, status pages, APM)

Part of GTM refresh to improve SEO and conversion.
2026-02-03 14:54:29 +00:00
Simon Larsen
a7c3ea274f Merge pull request #2263 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-03 08:13:39 +00:00
simlarsen
da324f49d9 chore: npm audit fix 2026-02-03 02:29:12 +00:00
Nawaz Dhandala
6fd9223ee9 refactor: Improve code formatting for better readability in UserSettings components 2026-02-02 19:36:32 +00:00
Nawaz Dhandala
c062d651e8 chore: Bump version number to 9.5.2 2026-02-02 19:35:12 +00:00
Nawaz Dhandala
1c264ac5a1 feat: Enhance User Settings with Incident On-Call Rules and related components 2026-02-02 19:31:58 +00:00
Nawaz Dhandala
8923a4bff6 feat: Format MicrosoftTeamsIntegration component for improved readability 2026-02-02 18:29:57 +00:00
Nawaz Dhandala
ab4e0cf85f chore: Bump version number to 9.5.1 2026-02-02 18:29:48 +00:00
Nawaz Dhandala
8f63e93eba feat: Replace ComingSoon component with MicrosoftTeamsIntegration for immediate functionality 2026-02-02 18:26:52 +00:00
Nawaz Dhandala
c06697f299 chore: Clean up whitespace in migration file for better readability 2026-02-02 18:21:51 +00:00
Nawaz Dhandala
2590a8d671 feat: Refactor DashboardSideMenu to use sections for better organization and readability 2026-02-02 18:20:55 +00:00
Nawaz Dhandala
e4eadc297b feat: Update Dashboard SideMenu to rename "Overview" to "Monitors" and conditionally add "Monitor Groups" section 2026-02-02 18:05:45 +00:00
Nawaz Dhandala
d0bf351dc1 feat: Add monitorId column and constraints to log tables in migration 2026-02-02 17:56:22 +00:00
Nawaz Dhandala
3234cc7d09 feat: Add migration for monitorId integration and schema updates across log tables 2026-02-02 17:46:50 +00:00
Nawaz Dhandala
0158271e6a feat: Add monitorId support across notification services and logs, and implement Notification Logs page 2026-02-02 17:44:25 +00:00
Nawaz Dhandala
9d50bf2535 chore: Bump version to 9.5.0 2026-02-01 16:19:28 +00:00
Nawaz Dhandala
c8ca2eacc5 fix: Improve descriptions for note reminder intervals and templates in IncidentSlaRule 2026-02-01 16:18:42 +00:00
Nawaz Dhandala
66699901a7 refactor: Simplify function signatures and improve code readability in Sla.tsx 2026-02-01 16:13:20 +00:00
Nawaz Dhandala
68a33eee2f feat: Enhance SLA management by integrating incident state timeline for accurate response and resolution tracking 2026-02-01 16:04:50 +00:00
Nawaz Dhandala
016793d77d feat: Implement SLA rule management with add and remove functionality in IncidentViewSla 2026-02-01 16:00:52 +00:00
Nawaz Dhandala
7b040b659d fix: Update SlaCard to consider incident resolution status for completion state 2026-02-01 15:56:54 +00:00
Nawaz Dhandala
c6db71f383 fix: Convert modelId and projectId to strings for stable dependency references in IncidentViewSla 2026-02-01 15:50:07 +00:00
Nawaz Dhandala
16923c750b feat: Add canReadOnRelationQuery property to IncidentSlaRule model and enhance SLA timers with real-time updates 2026-02-01 15:49:07 +00:00
Nawaz Dhandala
f57173f43c fix: Update sorting field in SLA tracking to use 'slaStartedAt' instead of 'createdAt' 2026-02-01 15:42:45 +00:00
Nawaz Dhandala
9716d138ea refactor: Update fetchStates function type annotation and simplify error handling in EpisodeAlerts and EpisodeIncidents components 2026-02-01 15:34:18 +00:00
Nawaz Dhandala
8e1b6859f5 feat: Add support for incident episode notification rule type and update default title generation to use 'Untitled Episode' 2026-02-01 15:28:38 +00:00
Nawaz Dhandala
9751dd0d5f refactor: Update episode title generation to remove prefix and use 'Untitled Episode' as default 2026-02-01 15:21:47 +00:00
Nawaz Dhandala
e84bd95f49 feat: Add canReadOnRelationQuery property to Alert and Incident models 2026-02-01 14:10:13 +00:00
Nawaz Dhandala
02b76539ab feat: Fetch and display alert and incident states in episode views 2026-02-01 14:07:46 +00:00
Nawaz Dhandala
c40c33773b feat: Add action buttons and current state display for alerts and incidents in episode views 2026-02-01 13:55:49 +00:00
Nawaz Dhandala
b0cebf9338 refactor: Improve code structure and readability in role assignment components 2026-02-01 13:47:39 +00:00
Nawaz Dhandala
674e35dc70 refactor: Improve code readability by formatting and simplifying filter/map functions in various components 2026-02-01 13:44:07 +00:00
Nawaz Dhandala
d5f42141a0 feat: Enhance Episode Member Role Assignments with user selection and role management 2026-02-01 13:41:54 +00:00
Nawaz Dhandala
8fb6da7d41 feat: Add Episode Member Role Assignments form field for incident grouping rules 2026-02-01 13:39:41 +00:00
Nawaz Dhandala
d14e77ee7f feat: Implement role assignment functionality for incident episodes with UI components 2026-02-01 13:29:31 +00:00
Nawaz Dhandala
71e8a70717 feat: Add IncidentEpisodeRoleMember service and model to BaseAPI feature set 2026-02-01 13:20:21 +00:00
Nawaz Dhandala
370bdc6e21 feat: Refactor MonitorCriteria components to use dedicated UI elements for teams, users, and labels 2026-02-01 13:09:07 +00:00
Nawaz Dhandala
d2a5d037c1 feat: Add support for labels, teams, users, and incident roles in monitor criteria components 2026-02-01 13:02:53 +00:00
Nawaz Dhandala
1ffa87d322 feat: Update title of My On-Call Policies card to reflect active assignments 2026-02-01 12:48:45 +00:00
Nawaz Dhandala
28ffde7983 feat: Enhance My On-Call Policies page with project details and improved UI components 2026-02-01 12:48:17 +00:00
Nawaz Dhandala
4655e207a5 feat: Add My On-Call Policies page and integrate routing 2026-02-01 12:44:00 +00:00
Nawaz Dhandala
62a5b216a0 feat: Update collapsible section titles for clarity in Monitor Criteria forms 2026-02-01 12:33:15 +00:00
Nawaz Dhandala
79fb9d18ca feat: Conditionally render Test Monitor Card based on monitor type 2026-02-01 12:28:23 +00:00
Nawaz Dhandala
ba5e8fdaeb feat: Simplify Monitor Criteria section by removing conditional rendering 2026-02-01 12:26:50 +00:00
Nawaz Dhandala
aa31bbab45 feat: Refactor Ownership & Labels section in MonitorCriteriaIncidentForm with improved dropdown handling 2026-02-01 12:22:27 +00:00
Nawaz Dhandala
3c84365d61 feat: Add reassign functionality for member roles with modal confirmation 2026-02-01 12:13:57 +00:00
Nawaz Dhandala
ae59bd8300 feat: Enhance incident member assignment logic to include primary role assignments for creators 2026-02-01 12:08:34 +00:00
Nawaz Dhandala
b868206e82 feat: Improve user assignment flow with enhanced dropdown handling and feedback 2026-02-01 12:05:23 +00:00
Nawaz Dhandala
ddbf971f1f feat: Reorder Owners item in SideMenu for improved accessibility 2026-02-01 12:01:35 +00:00
Nawaz Dhandala
483578ba4d refactor: Remove unused owner teams and users fields from incident creation 2026-02-01 12:00:41 +00:00
Nawaz Dhandala
614cb4413e feat: Enhance user assignment button with disabled state and tooltip for clarity 2026-02-01 11:57:47 +00:00
Nawaz Dhandala
ad43fc2df2 fix: Adjust icon size in MemberRoleAssignment component for better visibility 2026-02-01 11:54:44 +00:00
Nawaz Dhandala
aa2a6deb9e feat: Refactor role assignments to use useRef for better performance and update role assignment handling in IncidentCreate 2026-02-01 11:51:28 +00:00
Nawaz Dhandala
dcb13bb401 feat: Add showEvenIfPermissionDoesNotExist option to incident roles assignment 2026-02-01 11:40:00 +00:00
Nawaz Dhandala
5260364e91 fix lint 2026-01-30 20:38:36 +00:00
Nawaz Dhandala
1938e620bb feat: Update role assignment label to clarify multiple user assignment for incident roles 2026-01-30 20:32:27 +00:00
Nawaz Dhandala
c3fd71dcd4 feat: Enhance incident role assignment functionality with new IncidentRoleFormField component and support for multiple user assignments 2026-01-30 20:24:34 +00:00
Nawaz Dhandala
aba191c533 feat: Implement validation for primary roles to restrict multiple user assignments and update IncidentRoles component to include toggle for multiple user assignment 2026-01-30 20:09:31 +00:00
Nawaz Dhandala
847c019aea feat: Add canAssignMultipleUsers field to IncidentRole and update related components for multi-user assignment 2026-01-30 19:55:13 +00:00
Nawaz Dhandala
edf05944c1 feat: Add roleIcon to MemberRole and IncidentMemberRoleAssignment for enhanced role representation 2026-01-30 19:48:13 +00:00
Nawaz Dhandala
5293876943 feat: Update MemberRoleAssignment to allow single member assignment per role and improve user dropdown options 2026-01-30 19:42:51 +00:00
Nawaz Dhandala
ca7a702c13 feat: Add createdAt field to user selection in IncidentMemberRoleAssignment 2026-01-30 19:37:42 +00:00
Nawaz Dhandala
4020b4b647 feat: Update SideMenu section titles for clarity and consistency 2026-01-30 19:34:44 +00:00
Nawaz Dhandala
650849f4ad feat: Rename Members page to Roles and implement role assignment component 2026-01-30 19:09:06 +00:00
Nawaz Dhandala
9098261ac0 feat: Enhance user role assignment logic by validating role assignments and filtering available users by role 2026-01-30 19:07:28 +00:00
Nawaz Dhandala
70c6abbb86 feat: Add alert and incident episode counts with real-time updates in DashboardHeader 2026-01-30 14:15:52 +00:00
Nawaz Dhandala
23bc5531f0 feat: Add Incident SLA and Incident SLA Rule services and models to BaseAPI 2026-01-30 14:07:55 +00:00
Nawaz Dhandala
20404458e2 refactor: Remove dynamic imports of IncidentService and IncidentSlaService for improved performance 2026-01-30 14:00:44 +00:00
Nawaz Dhandala
31d3ce949d Refactor and clean up code across multiple files
- Added missing commas in migration index.
- Improved type annotations for dynamic imports in IncidentService and IncidentSlaRuleService.
- Simplified logger debug messages in IncidentSlaService and IncidentSlaRuleService.
- Cleaned up JSX formatting in IconPicker, NotificationBellDropdown, RoleLabel, and Header components.
- Enhanced readability by restructuring long lines and removing unnecessary line breaks in various components.
- Updated error handling in fetch functions within DashboardHeader to use concise catch blocks.
- Refactored UserSettings and Incident routes for better readability.
- Improved code consistency and formatting in CheckSlaBreaches job.
2026-01-30 13:50:50 +00:00
Nawaz Dhandala
61ed224ad0 feat: Add Incident SLA and related tables with migration 2026-01-30 13:39:46 +00:00
Nawaz Dhandala
71ea76ee62 feat: Implement Incident SLA Management System
- Added IncidentSlaStatus enum to define SLA status values.
- Created IncidentSlaRulesPage component for managing SLA rules, including documentation and configuration options.
- Developed IncidentViewSla component to display SLA status and deadlines for incidents.
- Implemented CheckSlaBreaches job to monitor SLA breaches and send notifications.
- Created SendNoteReminders job to automate internal and public note reminders based on SLA rules.
2026-01-30 13:03:48 +00:00
Nawaz Dhandala
7a4a0553ca fix: Update fetchData dependencies to use string representations of incidentId and projectId 2026-01-30 12:26:22 +00:00
Nawaz Dhandala
cb57fa4a07 feat: Add new icon types (Compass, Disc, Grid) to Icon component 2026-01-30 12:20:47 +00:00
Nawaz Dhandala
22bc222689 feat: Refactor IconPicker and RoleLabel components for improved icon handling and styling 2026-01-30 12:12:21 +00:00
Nawaz Dhandala
9d96170c42 feat: Add migration for roleIcon column in IncidentRole and update OnCallDutyPolicyScheduleLayer defaults 2026-01-30 12:03:01 +00:00
Nawaz Dhandala
d0f4d21177 feat: Change roleIcon type from string to IconProp in IncidentRole model 2026-01-30 11:59:18 +00:00
Nawaz Dhandala
e9a2167484 feat: Rename icon to roleIcon in IncidentRole model and update references in IncidentRoles and CustomFields components 2026-01-30 11:59:03 +00:00
Nawaz Dhandala
a8af991a80 feat: Add IconPicker component and integrate icon selection in IncidentRoles 2026-01-30 11:52:21 +00:00
Nawaz Dhandala
8bb3a5b7ac feat: Replace Pill with RoleLabel component in IncidentRoles for enhanced role display 2026-01-30 11:45:22 +00:00
Nawaz Dhandala
d0b1efb660 feat: Add isEditable prop to CustomFieldsDetail and set it in UserView 2026-01-30 11:34:24 +00:00
Nawaz Dhandala
4f3259c3b1 feat: Enhance user profile creation with HTTP response handling and validation 2026-01-30 11:31:42 +00:00
Nawaz Dhandala
30b53a90a4 feat: Reorder profile section in SideMenu for improved organization 2026-01-30 11:31:00 +00:00
Nawaz Dhandala
cf8377ceec feat: Add custom fields management for team members and user profiles
- Introduced new models: ProjectUserProfile and TeamMemberCustomField to manage custom fields for users in projects.
- Implemented services for ProjectUserProfile and TeamMemberCustomField for database interactions.
- Created UI components for managing team member custom fields and user-specific custom fields in settings.
- Updated routing to include new pages for custom fields management.
- Added necessary database migrations for new tables and relationships.
- Enhanced side menu in user settings to navigate to custom fields.
2026-01-30 11:28:00 +00:00
Nawaz Dhandala
ffc7dbc35f feat: Update user notification event type for incident episode creation 2026-01-30 11:01:23 +00:00
Nawaz Dhandala
97fd817db4 feat: Enhance change plan API with payment method validation and error handling 2026-01-30 10:50:19 +00:00
Nawaz Dhandala
aa7caaa193 feat: Add validation for payment provider customer and methods before changing plan 2026-01-30 10:48:08 +00:00
Nawaz Dhandala
a05853ea09 feat: Move NotificationBell component to the right section for improved visibility 2026-01-30 10:45:26 +00:00
Nawaz Dhandala
2ba96c093d feat: Update NotificationBell to improve badge count logic and display based on alert types 2026-01-30 10:43:56 +00:00
Nawaz Dhandala
ed3df77ca4 feat: Refactor NotificationBell and related components for improved alert handling and UI consistency 2026-01-30 10:39:35 +00:00
Nawaz Dhandala
9ec363d222 feat: Implement notification system with NotificationBell and related components 2026-01-30 10:29:33 +00:00
Nawaz Dhandala
a090ec2747 feat: Enhance migration and service files for incident roles and alert grouping
- Updated migration file to include new tables and constraints for incident roles and grouping rules.
- Refactored alert grouping engine service for improved readability.
- Adjusted incident episode role member service for better error logging and code clarity.
- Modified data migration to streamline imports and enhance maintainability.
2026-01-29 22:09:38 +00:00
Nawaz Dhandala
9fde4fece9 feat: Add migration for IncidentEpisodeRoleMember and related grouping rules 2026-01-29 22:03:40 +00:00
Nawaz Dhandala
596798801a feat: Add episode configuration fields and role assignments to incident and alert grouping rules 2026-01-29 21:59:13 +00:00
Nawaz Dhandala
dcc87c46b2 feat: Implement Incident Episode Role Member functionality and UI components 2026-01-29 21:51:49 +00:00
Nawaz Dhandala
33fdabaea3 feat: Add migration to assign default incident roles to existing projects 2026-01-29 21:41:30 +00:00
Nawaz Dhandala
35deea863b feat: Refactor incident member role assignment and improve code readability across components 2026-01-29 21:39:29 +00:00
Nawaz Dhandala
2b2bbbdd55 feat: Add incident member role assignment functionality to MonitorIncident and related components 2026-01-29 21:38:41 +00:00
Nawaz Dhandala
d8cd92c504 feat: Update title and description in ListByTag.ejs for improved SEO 2026-01-29 21:38:17 +00:00
Nawaz Dhandala
f6ef2fa97d feat: Add IncidentMemberRoleAssignment component for managing team member roles in incidents 2026-01-29 21:25:03 +00:00
Nawaz Dhandala
8a86f6a94f feat: Implement auto-assignment of Incident Commander during state changes 2026-01-29 21:14:33 +00:00
Nawaz Dhandala
4b30274915 feat: Add incident member notification system with email and WhatsApp templates 2026-01-29 21:09:44 +00:00
Nawaz Dhandala
01f7d7cc78 feat: Refactor routing and add ActiveIncidents and ActiveAlerts components 2026-01-29 20:59:58 +00:00
Nawaz Dhandala
616e64110a feat: Add MigrationName1769719826928 to implement isPrimaryRole and isDeleteable fields in IncidentRole 2026-01-29 20:51:00 +00:00
Nawaz Dhandala
903a72a4e1 feat: Add isPrimaryRole and isDeleteable fields to IncidentRole model and update related service logic 2026-01-29 20:50:17 +00:00
Nawaz Dhandala
699c1d4341 feat: Update MigrationName1769719135546 to include down method and fix formatting 2026-01-29 20:42:13 +00:00
Nawaz Dhandala
078a4e8180 Merge branch 'incident-roles' 2026-01-29 20:39:52 +00:00
Nawaz Dhandala
29232e7052 feat: Add new migration MigrationName1769719135546 to schema migrations 2026-01-29 20:39:29 +00:00
Nawaz Dhandala
a221f7247c feat: Remove startsAt and endsAt fields from IncidentMember model and update related migration 2026-01-29 20:39:12 +00:00
Simon Larsen
c4b5aca463 Merge pull request #2259 from OneUptime/incident-roles
feat: Add Incident Member and Role Management
2026-01-29 20:32:09 +00:00
Nawaz Dhandala
9de4be6661 chore: Remove unnecessary blank lines in SettingsRoutes component 2026-01-29 20:31:57 +00:00
Simon Larsen
a532dcdd5f Merge pull request #2260 from digitalsparky/patch-1
Fix installation command to ensure it can follow the redirect
2026-01-29 20:20:35 +00:00
Simon Larsen
bde09d2326 Merge pull request #2261 from digitalsparky/patch-2
Modify clone instructions for release branch
2026-01-29 20:19:40 +00:00
Matt Spurrier
b36ac68026 Modify clone instructions for release branch
Updated the instructions to clone the repository with only the release branch.

This will save significant bandwidth and disk space by cloning only the release branch as it exists now, rather than the entire repository with its full history.
2026-01-30 04:02:16 +08:00
Matt Spurrier
200a94692e Fix installation command to ensure it can follow the redirect
The install.The URL is a redirect, for curl to follow it, you must use the -L option, otherwise you get an error.

EG:

root@inf1:~# curl https://oneuptime.com/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   107  100   107    0     0    242      0 --:--:-- --:--:-- --:--:--   243
bash: line 1: Found.: command not found
2026-01-30 03:42:58 +08:00
Nawaz Dhandala
3afc3a3302 feat: Add IncidentRole and IncidentMember APIs to BaseAPIFeatureSet 2026-01-29 19:16:23 +00:00
Nawaz Dhandala
fa52c30462 feat: Add Incident Roles page and update routing in incidents and settings 2026-01-29 19:09:19 +00:00
Nawaz Dhandala
c229936d5c Refactor permission arrays in database models to improve formatting consistency
- Updated permission arrays in various database model files to ensure consistent formatting by aligning closing brackets.
- Adjusted the `onBeforeCreate` function in the IncidentMembers component for improved readability.
- Cleaned up descriptions in the Permission helper class for better clarity.
- Added a new migration for IncidentRole and IncidentMember tables with appropriate constraints and indexes.
2026-01-29 18:58:05 +00:00
Nawaz Dhandala
70b2fb8c16 feat: Add IncidentRole and IncidentMember migration with constraints and indexes 2026-01-29 18:54:51 +00:00
Nawaz Dhandala
264613c676 feat: Rename On-Call Users to Members and update related routes and components 2026-01-29 16:52:50 +00:00
Nawaz Dhandala
61209f967f refactor: Remove order field from IncidentRole model and related services 2026-01-29 16:50:59 +00:00
Nawaz Dhandala
f82de89f3f feat: Add Incident Member and Role Management
- Introduced IncidentMember model to manage users assigned to incidents with specific roles.
- Created IncidentRole model to define roles that can be assigned during incident response.
- Implemented IncidentMemberService for CRUD operations on incident members.
- Implemented IncidentRoleService for CRUD operations on incident roles with order management.
- Added OnCallUsers page to display and manage users assigned to incidents.
- Added IncidentRoles settings page to define and manage incident roles.
- Updated RouteMap to include new routes for on-call users and incident roles.
2026-01-29 16:33:52 +00:00
Nawaz Dhandala
b94a095bef feat: add ReadAllProjectResources permission to various models
- Updated TelemetryIngestionKey, TelemetryUsageBilling, User, WhatsAppLog, Workflow, WorkflowLog, WorkflowVariable, WorkspaceNotificationLog, WorkspaceNotificationRule, WorkspaceProjectAuthToken models to include the new ReadAllProjectResources permission in access control settings.
- Introduced ReadAllProjectResources permission in Permission.ts with a description outlining its purpose and scope.
2026-01-29 15:03:22 +00:00
Simon Larsen
c08de3da35 Merge pull request #2258 from OneUptime/monitor-ui
feat: Add support for labels and ownership in Monitor Alert and Incid…
2026-01-29 14:09:25 +00:00
Nawaz Dhandala
748e18fd1b fix: Correct type annotation for updateField function in MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm 2026-01-29 14:09:08 +00:00
Nawaz Dhandala
c0d7c34018 refactor: Improve code readability by formatting multiline expressions and consistent function signatures in Monitor components 2026-01-29 14:04:42 +00:00
Nawaz Dhandala
e249ee6e59 feat: Add support for labels and ownership in Monitor Alert and Incident forms
- Updated MonitorAlert and MonitorIncident classes to handle labels and owner teams/users.
- Enhanced CriteriaAlert and CriteriaIncident types to include labelIds, ownerTeamIds, and ownerUserIds.
- Modified MonitorCriteriaAlertForm and MonitorCriteriaIncidentForm components to support new fields for labels and ownership.
- Integrated dropdown options for labels, teams, and users in MonitorSteps and related components.
- Implemented fetching of labels, teams, and users in MonitorSteps for dynamic dropdowns.
2026-01-29 14:02:55 +00:00
Simon Larsen
9eca0153ce Merge pull request #2257 from OneUptime/monitor-ui
feat: Refactor MonitorStep component to use CollapsibleSection for ad…
2026-01-29 13:34:13 +00:00
Nawaz Dhandala
c4deb0d0b4 feat: Refactor MonitorStep component to use CollapsibleSection for advanced options and improve UI structure
- Introduced CollapsibleSection component for better organization of advanced options in MonitorStep.
- Replaced existing div structures with Card components for improved visual hierarchy.
- Enhanced user experience by adding collapsible sections for API and Website monitor advanced settings.
- Cleaned up code by removing unnecessary comments and consolidating error handling logic.
2026-01-29 13:28:33 +00:00
Simon Larsen
7ecd86eca7 Merge pull request #2256 from elmy-team/fix-missing-redis-existing-secret
Fix missing Redis & ClickHouse existing secrets
2026-01-29 12:09:37 +00:00
Matías Plaza
f3312a2417 fix(helm-chart): add missing existing secret condition for clickhouse 2026-01-29 10:52:33 +01:00
Nawaz Dhandala
e0558a4a0a chore: Bump version to 9.4.13 2026-01-29 09:07:31 +00:00
Nawaz Dhandala
e2a238e3e3 refactor: Simplify imports and improve type annotations in IncidentEpisode services 2026-01-29 09:06:32 +00:00
Nawaz Dhandala
543c62df5a feat: Implement AI-generated postmortem functionality for IncidentEpisode 2026-01-29 09:05:42 +00:00
Matías Plaza
382c838d40 fix(helm-chart): add missing existing secret condition for redis 2026-01-29 10:04:31 +01:00
Nawaz Dhandala
1a88832efc feat: Enhance EpisodePostmortem component with template functionality 2026-01-29 09:01:07 +00:00
Nawaz Dhandala
d1f97a3193 feat: Add canReadOnRelationQuery property to Incident model 2026-01-29 08:58:18 +00:00
Nawaz Dhandala
70a269b662 fix: Update icons in DashboardSideMenu for better clarity 2026-01-29 08:56:23 +00:00
Nawaz Dhandala
a4ae42fd08 feat: Add create episode button to IncidentEpisodesTable component 2026-01-29 08:54:05 +00:00
Simon Larsen
6c0161543a Merge pull request #2255 from OneUptime/snmp-monitor
Snmp monitor
2026-01-29 08:45:54 +00:00
Nawaz Dhandala
4519292cc8 feat: Add migration to rename 'eveluateOverTime' to 'evaluateOverTime' in Monitor table 2026-01-29 08:42:34 +00:00
Nawaz Dhandala
83993fc2a4 fix: Correct spelling of 'evaluateOverTime' in multiple criteria files 2026-01-29 08:38:35 +00:00
Simon Larsen
5340b04b26 Merge pull request #2253 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-01-29 08:28:58 +00:00
Nawaz Dhandala
2a6003e78f refactor: Add type annotations for functions in SnmpOidEditor and SnmpMonitor 2026-01-29 08:28:23 +00:00
Nawaz Dhandala
bfed03a10e refactor: Improve code formatting and readability in SNMP monitor components 2026-01-29 08:24:51 +00:00
Nawaz Dhandala
d99b20327f feat: Update SNMP monitor implementation and add net-snmp dependency 2026-01-29 08:22:04 +00:00
Nawaz Dhandala
3a317a8b55 feat: Enhance E2E test execution with retry mechanism 2026-01-29 08:09:32 +00:00
Nawaz Dhandala
0c64ba30b0 fix: Correct URL construction in generateBlogSitemapXml function 2026-01-29 08:08:40 +00:00
simlarsen
4400a7e5dd chore: npm audit fix 2026-01-29 02:15:00 +00:00
Nawaz Dhandala
45aab853c4 feat: Add SNMP monitor documentation with configuration and usage details 2026-01-28 23:26:10 +00:00
Nawaz Dhandala
b37c13d347 feat: Enhance monitor destination handling for SNMP monitors 2026-01-28 23:21:32 +00:00
Nawaz Dhandala
d13407494b feat: Implement SNMP monitor support with criteria and view components 2026-01-28 23:06:13 +00:00
Nawaz Dhandala
724ab97874 feat: Add SNMP Monitor functionality with v3 authentication support
- Introduced MonitorStepSnmpMonitor interface for SNMP configuration.
- Implemented SNMP version handling with enums for V1, V2c, and V3.
- Created utility classes for handling SNMP OIDs and v3 authentication parameters.
- Developed SNMP Monitor response structure to capture OID responses and status.
- Added UI components for SNMP Monitor configuration including OID editor and v3 auth form.
- Implemented SNMP query logic with retry mechanism and response time tracking.
- Added type definitions for net-snmp library to support SNMP operations.
2026-01-28 22:56:28 +00:00
Nawaz Dhandala
15e2fdcf48 feat: add IncidentGroupingRule service and router integration 2026-01-28 21:10:37 +00:00
Simon Larsen
3a546f9b5a Merge pull request #2254 from OneUptime/incident-episodes
feat: Add Incident Episode Management Pages and Components
2026-01-28 20:32:17 +00:00
Nawaz Dhandala
291a0f12f1 feat: Enhance Incident Grouping Rules Migration and UI Components
- Updated the migration script for IncidentGroupingRule to improve structure and readability.
- Added new foreign key constraints and indexes to enhance database integrity.
- Refactored DeleteAccount component to improve layout and readability.
- Improved placeholder text formatting in IncidentGroupingRules page for better clarity.
- Added ProjectService import in BillingService to resolve circular dependency.
- Refactored condition checks in IncidentGroupingEngineService for better readability.
2026-01-28 20:27:23 +00:00
Nawaz Dhandala
3d62b67bca feat: add IncidentGroupingRule migration with related tables and constraints 2026-01-28 19:55:03 +00:00
Nawaz Dhandala
4c31c2b651 feat: add incidentEpisodeId to multiple service classes 2026-01-28 19:42:44 +00:00
Nawaz Dhandala
acdcb2d5da feat: add IncidentGroupingRuleService and IncidentGroupingRules page
- Implemented IncidentGroupingRuleService to manage incident grouping rules with automatic deletion of old records if billing is enabled.
- Created IncidentGroupingRules page for managing incident grouping rules, including detailed documentation on grouping logic, match criteria, and configuration options.
- Added UI components for creating, editing, and displaying incident grouping rules with various filtering and grouping options.
2026-01-28 19:31:13 +00:00
Nawaz Dhandala
43084263ab feat: Add tabs for Incident and Incident Episodes in Microsoft Teams and Slack pages 2026-01-28 19:10:10 +00:00
Nawaz Dhandala
1bbc953462 Refactor and clean up code across multiple services and components
- Added missing commas in migration index.
- Removed unused imports in IncidentEpisodeService and UserNotificationRuleService.
- Simplified conditional statements and improved code readability in various services.
- Adjusted formatting for better consistency in code style.
- Enhanced error handling and logging in MicrosoftTeamsIncidentEpisodeActions.
- Updated documentation text in IncidentEpisodeDocs for clarity.
- Improved API call structure in IncidentEpisodesTable and IncidentEpisodeFeed.
- Refactored SlackIncidentEpisodeActions for better readability.
- Cleaned up unnecessary code and improved formatting in various components.
2026-01-28 18:49:22 +00:00
Nawaz Dhandala
7cef3956e8 feat: Add IncidentEpisode migration with related tables and constraints 2026-01-28 18:48:36 +00:00
Nawaz Dhandala
4904a535d1 feat: Add Active Incident Episodes page and integrate with SideMenu 2026-01-28 18:47:26 +00:00
Nawaz Dhandala
5db511036e feat: Add notification handling for Incident Episodes and enhance Push Notification utility 2026-01-28 18:40:39 +00:00
Nawaz Dhandala
8b9023d93d feat: Add incident episode auto-resolution and notification jobs
- Implemented AutoResolve job to automatically resolve inactive incident episodes.
- Created ResolveInactiveEpisodes job to resolve episodes inactive for over 24 hours.
- Added SendCreatedResourceNotification job to notify owners of newly created incident episodes.
- Developed SendNotePostedNotification job to inform owners when a note is posted on an incident episode.
- Introduced SendOwnerAddedNotification job to notify users when they are added as owners of an incident episode.
- Implemented SendStateChangeNotification job to alert owners of state changes in incident episodes.
2026-01-28 18:30:45 +00:00
Nawaz Dhandala
0546d1fb12 feat: Enhance User Notification and OnCall Duty Policy services to support Incident Episode handling 2026-01-28 18:13:09 +00:00
Nawaz Dhandala
d523ae822d feat: Implement Incident Episode services, routes, and notification templates 2026-01-28 18:07:36 +00:00
Nawaz Dhandala
9fd781c083 feat: Add Slack and Workspace message handling for Incident Episodes
- Implemented SlackIncidentEpisodeMessages class to create message blocks for incident episodes in Slack.
- Added IncidentEpisodeWorkspaceMessages class to handle workspace notifications and message blocks for incident episodes.
- Created IncidentEpisodeDocs component to provide a comprehensive guide on incident grouping, including lifecycle, setup steps, and best practices.
2026-01-28 18:00:10 +00:00
Nawaz Dhandala
8d743dbb59 feat: Add Incident Episode creation functionality and related routes 2026-01-28 17:19:15 +00:00
Nawaz Dhandala
5b3e97c10d feat: Add Incident Episode Management Pages and Components
- Implemented Episode Internal Note component for managing private notes related to incident episodes.
- Created Layout component for the Incident Episode view, integrating side menu and breadcrumb navigation.
- Developed Owners component to manage teams and users associated with incident episodes.
- Added Postmortem component for documenting postmortem analyses of incidents.
- Introduced Remediation component for capturing remediation notes for incidents.
- Created Root Cause component to document the root causes of incidents.
- Developed SideMenu for navigating through various sections of the Incident Episode view.
- Implemented State Timeline component to track the status changes of incident episodes.
- Added Episodes and Unresolved Episodes pages for listing all incident episodes and unresolved ones respectively.
2026-01-28 17:10:08 +00:00
Nawaz Dhandala
252a81c9ae chore: bump version to 9.4.12 2026-01-28 15:50:27 +00:00
Nawaz Dhandala
e6b414a94b feat: enhance project fetching in NewAlerts and NewIncidents components with ListResult type 2026-01-28 15:50:15 +00:00
Nawaz Dhandala
f521091f8e feat: update project filter to use projectId and change filter type to EntityArray in NewAlerts and NewIncidents components 2026-01-28 15:46:08 +00:00
Nawaz Dhandala
19e112a8a8 feat: add project filter functionality to NewAlerts and NewIncidents components 2026-01-28 15:33:35 +00:00
Nawaz Dhandala
84dd084dae feat: add saveFilterProps to NewAlerts and NewIncidents components for improved table filtering 2026-01-28 13:08:56 +00:00
Nawaz Dhandala
439c1f8716 style: format code for improved readability in EpisodeCreate component 2026-01-28 12:59:00 +00:00
Nawaz Dhandala
647b713375 feat: add episode creation functionality with form and routing 2026-01-28 12:58:16 +00:00
Nawaz Dhandala
a2e6b7a4fc feat: add Stripe type definitions and improve type safety in Billing and Project services 2026-01-28 12:09:41 +00:00
Nawaz Dhandala
b06bc71a2c refactor: improve logging and code formatting in billing and project services 2026-01-28 12:08:01 +00:00
Nawaz Dhandala
367a80c413 fix: remove unused status field from invoice template and billing service 2026-01-28 12:05:03 +00:00
Nawaz Dhandala
f49e4bd5d0 feat: implement invoice email notification system with template and email handling 2026-01-28 12:01:28 +00:00
Nawaz Dhandala
a74a7e0a9a feat: enhance billing functionality with detailed logging for invoice email processes and Stripe synchronization 2026-01-28 11:52:34 +00:00
Nawaz Dhandala
f4946449f3 feat: add migration to introduce sendInvoicesByEmail column and update defaults for OnCallDutyPolicyScheduleLayer 2026-01-28 11:32:44 +00:00
Nawaz Dhandala
9640732e29 feat: implement Stripe webhook for automatic invoice email sending and add configuration for webhook secret 2026-01-28 11:26:22 +00:00
Nawaz Dhandala
6ef5e409da fix: replace Navigation with window.location for redirect after account deletion 2026-01-28 09:53:57 +00:00
Nawaz Dhandala
bcdfa034f6 feat: add user account deletion functionality with confirmation modal 2026-01-28 09:50:25 +00:00
Nawaz Dhandala
959267a174 feat: update NewAlerts and NewIncidents to use alertNumber and incidentNumber with appropriate types 2026-01-28 09:40:42 +00:00
Nawaz Dhandala
767db415d2 fix: conditionally include incidentNumber in notification payloads 2026-01-27 21:11:50 +00:00
Nawaz Dhandala
7ca81aa9f8 chore: bump version to 9.4.11 2026-01-27 20:42:20 +00:00
Nawaz Dhandala
26bd4c7a90 fix: improve readability of conditional check in Delete method 2026-01-27 20:42:00 +00:00
Nawaz Dhandala
9d29a1d00b Refactor Terraform tests to remove project_id references
- Removed project_id variable and its usage from multiple Terraform test files (23-probe-crud, 24-status-page-crud, 25-status-page-with-domain, 26-monitor-steps-basic, 27-monitor-types, 28-incident-crud, 29-alert-crud, 30-scheduled-maintenance-crud, 31-on-call-duty-policy-crud, 32-monitor-group-crud, 33-team-crud, 35-monitor-with-steps, 36-monitor-types-basic, 37-label-order-idempotency).
- Updated ResourceGenerator to treat project_id as computed-only, inferred from API key authentication.
- Adjusted related logic in resource update and delete operations to exclude project_id from requests.
2026-01-27 20:00:41 +00:00
961 changed files with 127866 additions and 10143 deletions

View File

@@ -389,6 +389,23 @@ jobs:
max_attempts: 3
command: cd MCP && npm update @oneuptime/common && npm install && npm run compile && npm run dep-check
compile-mobile-app:
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 && npm run compile
- name: Compile MobileApp
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npm install && npm run compile
compile-ai-agent:
runs-on: ubuntu-latest
env:
@@ -404,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

View File

@@ -1913,12 +1913,51 @@ jobs:
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
# Aggressively free disk space before anything else
- name: Aggressive Disk Cleanup
run: |
echo "=== Disk space BEFORE cleanup ==="
df -h /
# Remove pre-installed software not needed for this job
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /opt/hostedtoolcache || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/graalvm/ || true
sudo rm -rf /usr/local/share/powershell || true
sudo rm -rf /usr/local/share/chromium || true
sudo rm -rf /usr/local/lib/node_modules || true
sudo rm -rf /usr/share/swift || true
sudo rm -rf /usr/share/miniconda || true
sudo rm -rf /usr/lib/google-cloud-sdk || true
sudo rm -rf /usr/lib/jvm || true
sudo rm -rf /usr/lib/firefox || true
sudo rm -rf /usr/lib/heroku || true
sudo rm -rf /usr/local/julia* || true
sudo rm -rf /opt/az || true
sudo rm -rf /opt/microsoft || true
sudo rm -rf /opt/pipx || true
sudo rm -rf /opt/actionarchivecache || true
sudo rm -rf /imagegeneration || true
sudo rm -rf /usr/share/az_* || true
sudo rm -rf /usr/share/sbt || true
sudo rm -rf /usr/share/gradle* || true
sudo rm -rf /usr/share/kotlinc || true
sudo rm -rf /usr/share/ri || true
sudo rm -rf /usr/local/.ghcup || true
# Clean apt cache
sudo apt-get clean || true
sudo rm -rf /var/lib/apt/lists/* || true
# Clean temp files
sudo rm -rf /tmp/* || true
# Docker cleanup
docker system prune -af --volumes || true
echo "=== Disk space AFTER aggressive cleanup ==="
df -h /
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
android: true
dotnet: true
@@ -1926,6 +1965,10 @@ jobs:
large-packages: true
docker-images: true
swap-storage: true
- name: Final Disk Space Check
run: |
echo "=== Disk space after all cleanup ==="
df -h /
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
@@ -2001,12 +2044,51 @@ jobs:
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
# Aggressively free disk space before anything else
- name: Aggressive Disk Cleanup
run: |
echo "=== Disk space BEFORE cleanup ==="
df -h /
# Remove pre-installed software not needed for this job
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /opt/hostedtoolcache || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/local/graalvm/ || true
sudo rm -rf /usr/local/share/powershell || true
sudo rm -rf /usr/local/share/chromium || true
sudo rm -rf /usr/local/lib/node_modules || true
sudo rm -rf /usr/share/swift || true
sudo rm -rf /usr/share/miniconda || true
sudo rm -rf /usr/lib/google-cloud-sdk || true
sudo rm -rf /usr/lib/jvm || true
sudo rm -rf /usr/lib/firefox || true
sudo rm -rf /usr/lib/heroku || true
sudo rm -rf /usr/local/julia* || true
sudo rm -rf /opt/az || true
sudo rm -rf /opt/microsoft || true
sudo rm -rf /opt/pipx || true
sudo rm -rf /opt/actionarchivecache || true
sudo rm -rf /imagegeneration || true
sudo rm -rf /usr/share/az_* || true
sudo rm -rf /usr/share/sbt || true
sudo rm -rf /usr/share/gradle* || true
sudo rm -rf /usr/share/kotlinc || true
sudo rm -rf /usr/share/ri || true
sudo rm -rf /usr/local/.ghcup || true
# Clean apt cache
sudo apt-get clean || true
sudo rm -rf /var/lib/apt/lists/* || true
# Clean temp files
sudo rm -rf /tmp/* || true
# Docker cleanup
docker system prune -af --volumes || true
echo "=== Disk space AFTER aggressive cleanup ==="
df -h /
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
android: true
dotnet: true
@@ -2014,6 +2096,10 @@ jobs:
large-packages: true
docker-images: true
swap-storage: true
- name: Final Disk Space Check
run: |
echo "=== Disk space after all cleanup ==="
df -h /
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
@@ -2200,6 +2286,145 @@ jobs:
tag_name: ${{needs.read-version.outputs.major_minor}}
# Build Android release APK and attach to GitHub Release.
# Required secrets setup guide: MobileApp/docs/RELEASE_SIGNING.md
mobile-app-android-deploy:
needs: [draft-github-release, generate-build-number, read-version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Install dependencies
run: cd MobileApp && npm install
- name: Decode Android keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > /tmp/release.keystore
- name: Build release APK
env:
ANDROID_KEYSTORE_FILE: /tmp/release.keystore
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
cd MobileApp/android
./gradlew assembleRelease \
-PversionName=${{ needs.read-version.outputs.major_minor }} \
-PversionCode=${{ needs.generate-build-number.outputs.build_number }}
- name: Upload APK to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: MobileApp/android/app/build/outputs/apk/release/*.apk
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
prerelease: false
tag_name: ${{ needs.read-version.outputs.major_minor }}
# Build iOS release IPA and attach to GitHub Release.
# Required secrets setup guide: MobileApp/docs/RELEASE_SIGNING.md
mobile-app-ios-deploy:
needs: [draft-github-release, generate-build-number, read-version]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Install dependencies
run: cd MobileApp && npm install
- name: Install CocoaPods dependencies
run: cd MobileApp/ios && pod install
- name: Import signing certificate
env:
IOS_DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }}
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/distribution.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo "$IOS_DISTRIBUTION_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
- name: Install provisioning profile
env:
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
PROFILE_PATH=$RUNNER_TEMP/profile.mobileprovision
echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build archive
run: |
cd MobileApp
xcodebuild -workspace ios/OneUptime.xcworkspace \
-scheme OneUptime \
-configuration Release \
-sdk iphoneos \
-archivePath $RUNNER_TEMP/OneUptime.xcarchive \
archive \
MARKETING_VERSION=${{ needs.read-version.outputs.major_minor }} \
CURRENT_PROJECT_VERSION=${{ needs.generate-build-number.outputs.build_number }}
- name: Prepare ExportOptions.plist with team ID
env:
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
run: |
/usr/libexec/PlistBuddy -c "Add :teamID string $IOS_TEAM_ID" MobileApp/ios/ExportOptions.plist || \
/usr/libexec/PlistBuddy -c "Set :teamID $IOS_TEAM_ID" MobileApp/ios/ExportOptions.plist
- name: Export IPA
run: |
cd MobileApp
xcodebuild -exportArchive \
-archivePath $RUNNER_TEMP/OneUptime.xcarchive \
-exportOptionsPlist ios/ExportOptions.plist \
-exportPath $RUNNER_TEMP/build
- name: Upload IPA to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: ${{ runner.temp }}/build/*.ipa
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
prerelease: false
tag_name: ${{ needs.read-version.outputs.major_minor }}
- name: Cleanup keychain
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
finalize-github-release:
name: Publish GitHub release
needs: [infrastructure-agent-deploy, generate-build-number, read-version]

View File

@@ -134,6 +134,10 @@ jobs:
terraform_wrapper: false
- name: Run E2E Tests
run: |
chmod +x ./E2E/Terraform/e2e-tests/scripts/*.sh
./E2E/Terraform/e2e-tests/scripts/index.sh
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
max_attempts: 3
command: |
chmod +x ./E2E/Terraform/e2e-tests/scripts/*.sh
./E2E/Terraform/e2e-tests/scripts/index.sh

21
.github/workflows/test.cli.yaml vendored Normal file
View 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

39
.github/workflows/test.mobile-app.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: MobileApp Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
expo-doctor:
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 MobileApp && npm install
- name: Run Expo Doctor
run: cd MobileApp && npx expo-doctor@latest
expo-web-export:
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 MobileApp && npm install
- name: Export Web Bundle
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npx expo export --platform web

View File

@@ -73,6 +73,7 @@
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"expo-server-sdk": "^3.15.0",
"express": "^4.21.1",
"formik": "^2.4.6",
"history": "^5.3.0",

View File

@@ -1,5 +1,6 @@
import AuthenticationServiceHandler from "./Service/Authentication";
import DataTypeServiceHandler from "./Service/DataType";
import DataTypeDetailServiceHandler from "./Service/DataTypeDetail";
import ErrorServiceHandler from "./Service/Errors";
import OpenAPIServiceHandler from "./Service/OpenAPI";
import IntroductionServiceHandler from "./Service/Introduction";
@@ -10,6 +11,7 @@ import PermissionServiceHandler from "./Service/Permissions";
import StatusServiceHandler from "./Service/Status";
import { StaticPath } from "./Utils/Config";
import ResourceUtil, { ModelDocumentation } from "./Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "./Utils/DataTypes";
import Dictionary from "Common/Types/Dictionary";
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, {
@@ -24,6 +26,9 @@ const APIReferenceFeatureSet: FeatureSet = {
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
const DataTypeDictionary: Dictionary<DataTypeDocumentation> =
DataTypeUtil.getDataTypeDictionaryByPath();
const app: ExpressApplication = Express.getExpressApp();
// Serve static files for the API reference with a cache max age of 30 days
@@ -72,6 +77,8 @@ const APIReferenceFeatureSet: FeatureSet = {
return StatusServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "data-types") {
return DataTypeServiceHandler.executeResponse(req, res);
} else if (DataTypeDictionary[page]) {
return DataTypeDetailServiceHandler.executeResponse(req, res);
} else if (currentResource) {
return ModelServiceHandler.executeResponse(req, res);
}

View File

@@ -1,11 +1,13 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Retrieve resources documentation
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -27,6 +29,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,12 +1,14 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -153,6 +155,7 @@ export default class ServiceHandler {
pageDescription:
"Data Types that can be used to interact with OneUptime API",
resources: Resources,
dataTypes: DataTypes,
pageData: pageData,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// Handles the HTTP response for a given request
@@ -28,6 +30,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,11 +1,13 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Get all resources and featured resources from ResourceUtil
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
const FeaturedResources: Array<ModelDocumentation> =
ResourceUtil.getFeaturedResources();
@@ -34,6 +36,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -3,8 +3,10 @@ import CodeExampleGenerator, {
CodeExamples,
} from "../Utils/CodeExampleGenerator";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import PageNotFoundServiceHandler from "./PageNotFound";
import { AppApiRoute } from "Common/ServiceRoute";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import { ColumnAccessControl } from "Common/Types/BaseDatabase/AccessControl";
import {
getTableColumns,
@@ -314,9 +316,13 @@ function generateApiCodeExamples(
// Get all resources and resource dictionary
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
// Dynamically built from DataTypes registry — no manual updates needed when new types are added
const TypeToDocPath: Dictionary<string> = DataTypeUtil.getTypeToDocPathMap();
// Get all permission props
const PermissionDictionary: Dictionary<PermissionProps> =
PermissionHelper.getAllPermissionPropsAsDictionary();
@@ -392,6 +398,32 @@ export default class ServiceHandler {
delete tableColumns["deletedByUser"];
delete tableColumns["version"];
// For columns with a modelType (Entity/EntityArray), resolve the related model's documentation path
for (const key in tableColumns) {
const column: TableColumnMetadata | undefined = tableColumns[key];
if (column?.modelType) {
try {
const relatedModelInstance: BaseModel = new column.modelType();
if (relatedModelInstance.enableDocumentation) {
(column as any).modelDocumentationPath =
relatedModelInstance.getAPIDocumentationPath();
(column as any).modelName = relatedModelInstance.singularName;
}
} catch {
// If model instantiation fails, skip linking
}
}
// Resolve non-entity complex types to their documentation paths
if (column?.type && !(column as any).modelDocumentationPath) {
const typeStr: string = column.type.toString();
const docPath: string | undefined = TypeToDocPath[typeStr];
if (docPath) {
(column as any).typeDocumentationPath = docPath;
}
}
}
// Set page data
pageData["title"] = currentResource.model.singularName;
pageData["description"] = currentResource.model.tableDescription;
@@ -586,6 +618,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -5,12 +5,14 @@ import {
} from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import URL from "Common/Types/API/URL";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// Handles the HTTP response for a given request
@@ -36,6 +38,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,9 +1,11 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get an array of model documentation resources
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// This is a static method that handles the response
@@ -21,6 +23,7 @@ export default class ServiceHandler {
enableGoogleTagManager: IsBillingEnabled,
pageDescription: "Page you're looking for is not found.", // The page description
resources: Resources, // The array of model documentation resources
dataTypes: DataTypes,
pageData: {}, // An empty object to hold any additional page data
});
}

View File

@@ -1,12 +1,14 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get all resources from ResourceUtil
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -49,6 +51,7 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page, // Pass the page parameter
resources: Resources, // Pass all resources
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled, // Pass the page title
pageDescription: pageDescription, // Pass the page description

View File

@@ -1,11 +1,17 @@
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import { PermissionHelper, PermissionProps } from "Common/Types/Permission";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import {
PermissionGroup,
PermissionHelper,
PermissionProps,
} from "Common/Types/Permission";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -25,16 +31,39 @@ export default class ServiceHandler {
pageDescription = "Learn how permissions work with OneUptime";
// Filter permissions to only include those assignable to tenants
pageData["permissions"] = PermissionHelper.getAllPermissionProps().filter(
(i: PermissionProps) => {
const tenantPermissions: Array<PermissionProps> =
PermissionHelper.getAllPermissionProps().filter((i: PermissionProps) => {
return i.isAssignableToTenant;
},
);
});
// Group permissions by PermissionGroup
const permissionGroups: Array<{
group: string;
permissions: Array<PermissionProps>;
}> = [];
for (const group of Object.values(PermissionGroup)) {
const groupPermissions: Array<PermissionProps> = tenantPermissions.filter(
(p: PermissionProps) => {
return p.group === group;
},
);
if (groupPermissions.length > 0) {
permissionGroups.push({
group: group,
permissions: groupPermissions,
});
}
}
pageData["permissionGroups"] = permissionGroups;
// Render the page
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,10 +1,12 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
// Retrieve resources from ResourceUtil
const resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const dataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -21,6 +23,7 @@ export default class ServiceHandler {
enableGoogleTagManager: IsBillingEnabled,
pageDescription: "200 - Success",
resources: resources, // Pass resources to the template
dataTypes: dataTypes,
pageData: {}, // Pass empty data to the template
});
}

View File

@@ -0,0 +1,422 @@
import Dictionary from "Common/Types/Dictionary";
export interface DataTypeDocumentation {
name: string;
path: string;
description: string;
/*
* Additional column type display strings that should link to this data type page.
* Used for cases where the TableColumnType enum value doesn't match the PascalCase name
* (e.g., enum "Date" should link to the "DateTime" data type page).
*/
columnTypeAliases?: Array<string>;
/*
* Category for grouping in sidebar navigation.
* Types with the same category are grouped under a collapsible heading.
*/
category?: string;
}
export interface DataTypeCategory {
name: string;
types: Array<DataTypeDocumentation>;
}
export default class DataTypeUtil {
public static getDataTypes(): Array<DataTypeDocumentation> {
return [
{
name: "ObjectID",
path: "object-id",
description:
"A unique identifier for objects, typically a UUID string.",
},
{
name: "Decimal",
path: "decimal",
description: "A decimal number type for precise numeric values.",
},
{
name: "Name",
path: "name",
description: "A structured name type representing a text name value.",
},
{
name: "EqualTo",
path: "equal-to",
description:
"A query filter that matches objects where a field is equal to the specified value.",
},
{
name: "EqualToOrNull",
path: "equal-to-or-null",
description:
"A query filter that matches objects where a field is equal to the specified value or is null.",
},
{
name: "MonitorSteps",
path: "monitor-steps",
description:
"Complex nested object describing monitor check configuration including steps and default status.",
category: "Monitor",
},
{
name: "MonitorStep",
path: "monitor-step",
description:
"A single monitor step defining a check target, request configuration, and criteria for determining status.",
category: "Monitor",
},
{
name: "Recurring",
path: "recurring",
description:
"Object describing a recurring interval schedule (e.g., every 5 minutes, daily).",
},
{
name: "RestrictionTimes",
path: "restriction-times",
description:
"Object describing on-call duty time restrictions (daily or weekly windows).",
},
{
name: "MonitorCriteria",
path: "monitor-criteria",
description:
"A collection of monitor criteria instances used to evaluate monitor check results.",
category: "Monitor",
},
{
name: "PositiveNumber",
path: "positive-number",
description: "A number type that must be greater than zero.",
columnTypeAliases: ["Small Positive Number", "Big Positive Number"],
},
{
name: "MonitorCriteriaInstance",
path: "monitor-criteria-instance",
description:
"A single criteria rule defining conditions and the resulting monitor status when conditions are met.",
category: "Monitor",
},
{
name: "NotEqual",
path: "not-equal",
description:
"A query filter that matches objects where a field is not equal to the specified value.",
},
{
name: "Email",
path: "email",
description: "An email address type with built-in format validation.",
},
{
name: "Phone",
path: "phone",
description: "A phone number type with built-in format validation.",
},
{
name: "Color",
path: "color",
description:
"A color value represented as a hex string (e.g., #3498db).",
},
{
name: "Domain",
path: "domain",
description: "A domain name type (e.g., example.com).",
},
{
name: "Version",
path: "version",
description: "A semantic version type (e.g., 1.0.0).",
},
{
name: "IP",
path: "ip",
description:
"An IP address type supporting both IPv4 and IPv6 formats.",
},
{
name: "Route",
path: "route",
description: "A URL route/path segment type.",
},
{
name: "URL",
path: "url",
description: "A full URL type with protocol, host, and path.",
},
{
name: "Permission",
path: "permission",
description:
"A string identifier representing an access control permission in OneUptime.",
},
{
name: "Search",
path: "search",
description:
"A query filter for text search that matches objects containing the specified string.",
},
{
name: "GreaterThan",
path: "greater-than",
description:
"A query filter that matches objects where a field is greater than the specified value.",
},
{
name: "GreaterThanOrEqual",
path: "greater-than-or-equal",
description:
"A query filter that matches objects where a field is greater than or equal to the specified value.",
},
{
name: "GreaterThanOrNull",
path: "greater-than-or-null",
description:
"A query filter that matches objects where a field is greater than the specified value or is null.",
},
{
name: "LessThanOrNull",
path: "less-than-or-null",
description:
"A query filter that matches objects where a field is less than the specified value or is null.",
},
{
name: "LessThan",
path: "less-than",
description:
"A query filter that matches objects where a field is less than the specified value.",
},
{
name: "LessThanOrEqual",
path: "less-than-or-equal",
description:
"A query filter that matches objects where a field is less than or equal to the specified value.",
},
{
name: "Port",
path: "port",
description: "A network port number type (1-65535).",
},
{
name: "Hostname",
path: "hostname",
description: "A hostname type (e.g., api.example.com).",
},
{
name: "HashedString",
path: "hashed-string",
description:
"A string that is stored in hashed form. Used for sensitive data like passwords and API keys.",
},
{
name: "DateTime",
path: "date-time",
description:
"An ISO 8601 date-time string (e.g., 2024-01-15T10:30:00.000Z).",
columnTypeAliases: ["Date"],
},
{
name: "Buffer",
path: "buffer",
description:
"A binary data buffer, typically base64-encoded when serialized to JSON.",
},
{
name: "InBetween",
path: "in-between",
description:
"A query filter that matches objects where a field value is between two specified values (inclusive).",
},
{
name: "NotNull",
path: "not-null",
description:
"A query filter that matches objects where a field is not null.",
},
{
name: "IsNull",
path: "is-null",
description:
"A query filter that matches objects where a field is null.",
},
{
name: "Includes",
path: "includes",
description:
"A query filter that matches objects where a field value is included in the specified array of values.",
},
{
name: "DashboardComponent",
path: "dashboard-component",
description:
"A configuration object for a dashboard component including its type, layout, and settings.",
},
{
name: "DashboardViewConfig",
path: "dashboard-view-config",
description:
"A configuration object for a dashboard view including its components and layout.",
},
{
name: "CriteriaFilter",
path: "criteria-filter",
description:
"A single filter condition within a MonitorCriteriaInstance that defines what to check and how to compare it.",
category: "Monitor",
},
{
name: "CriteriaIncident",
path: "criteria-incident",
description:
"Configuration for an incident that is automatically created when a MonitorCriteriaInstance's conditions are met.",
category: "Monitor",
},
{
name: "CriteriaAlert",
path: "criteria-alert",
description:
"Configuration for an alert that is automatically created when a MonitorCriteriaInstance's conditions are met.",
category: "Monitor",
},
{
name: "CheckOn",
path: "check-on",
description:
"Enum specifying what aspect of a monitor response to evaluate (e.g., response code, response time, body content).",
category: "Monitor",
},
{
name: "FilterType",
path: "filter-type",
description:
"Enum specifying the comparison operator used in a CriteriaFilter (e.g., Equal To, Greater Than, Contains).",
category: "Monitor",
},
{
name: "FilterCondition",
path: "filter-condition",
description:
"Enum specifying how multiple filters are combined: 'All' (AND) or 'Any' (OR).",
category: "Monitor",
},
{
name: "MonitorStepLogMonitor",
path: "monitor-step-log-monitor",
description:
"Configuration for a Log monitor step, defining which logs to query and evaluate.",
category: "Monitor",
},
{
name: "MonitorStepTraceMonitor",
path: "monitor-step-trace-monitor",
description:
"Configuration for a Trace monitor step, defining which spans to query and evaluate.",
category: "Monitor",
},
{
name: "MonitorStepMetricMonitor",
path: "monitor-step-metric-monitor",
description:
"Configuration for a Metric monitor step, defining which metrics to query and evaluate.",
category: "Monitor",
},
{
name: "MonitorStepSnmpMonitor",
path: "monitor-step-snmp-monitor",
description:
"Configuration for an SNMP monitor step, defining the SNMP device connection and OIDs to query.",
category: "Monitor",
},
];
}
public static getDataTypesByCategory(): Array<DataTypeCategory> {
const allTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
const uncategorized: Array<DataTypeDocumentation> = [];
const categoryMap: Dictionary<Array<DataTypeDocumentation>> = {};
const categoryOrder: Array<string> = [];
for (const dt of allTypes) {
if (dt.category) {
if (!categoryMap[dt.category]) {
categoryMap[dt.category] = [];
categoryOrder.push(dt.category);
}
categoryMap[dt.category]!.push(dt);
} else {
uncategorized.push(dt);
}
}
const result: Array<DataTypeCategory> = [];
// Add uncategorized types first under "General"
if (uncategorized.length > 0) {
result.push({ name: "General", types: uncategorized });
}
// Add categorized groups
for (const cat of categoryOrder) {
if (categoryMap[cat]) {
result.push({ name: cat, types: categoryMap[cat]! });
}
}
return result;
}
public static getDataTypeDictionaryByPath(): Dictionary<DataTypeDocumentation> {
const dict: Dictionary<DataTypeDocumentation> = {};
for (const dataType of DataTypeUtil.getDataTypes()) {
dict[dataType.path] = dataType;
}
return dict;
}
/*
* Convert PascalCase name to space-separated display string.
* e.g., "ObjectID" → "Object ID", "MonitorSteps" → "Monitor Steps",
* "HashedString" → "Hashed String", "IP" → "IP"
*/
private static pascalCaseToDisplayString(name: string): string {
return name
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
}
/*
* Build a mapping from column type display strings to data type page paths.
* Automatically derives both PascalCase and display-string variants from each
* data type's name, so adding a new entry to getDataTypes() is all that's needed.
*/
public static getTypeToDocPathMap(): Dictionary<string> {
const map: Dictionary<string> = {};
for (const dt of DataTypeUtil.getDataTypes()) {
// Map PascalCase name: "ObjectID" → "object-id"
map[dt.name] = dt.path;
// Map display string: "Object ID" → "object-id"
const displayName: string = DataTypeUtil.pascalCaseToDisplayString(
dt.name,
);
if (displayName !== dt.name) {
map[displayName] = dt.path;
}
// Map any explicit aliases (for edge cases like enum "Date" → "date-time")
if (dt.columnTypeAliases) {
for (const alias of dt.columnTypeAliases) {
map[alias] = dt.path;
}
}
}
return map;
}
}

View File

@@ -73,6 +73,7 @@
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"expo-server-sdk": "^3.15.0",
"express": "^4.21.1",
"formik": "^2.4.6",
"history": "^5.3.0",
@@ -278,6 +279,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz",
"integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.23.5",
@@ -1390,7 +1392,8 @@
"node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"peer": true
},
"node_modules/@types/prettier": {
"version": "2.7.3",
@@ -1661,6 +1664,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
@@ -4008,6 +4012,7 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",

View File

@@ -0,0 +1,179 @@
<main class="py-12">
<article class="prose">
<!-- Type Hierarchy Breadcrumb -->
<% if (pageData.typeHierarchy && pageData.typeHierarchy.length > 0) { %>
<nav class="mb-6">
<ol class="flex items-center flex-wrap gap-1 text-sm">
<% for (var h = 0; h < pageData.typeHierarchy.length; h++) { %>
<% if (h > 0) { %>
<li class="text-slate-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</li>
<% } %>
<% if (pageData.typeHierarchy[h].path && pageData.typeHierarchy[h].name !== pageData.title) { %>
<li><a href="/reference/<%= pageData.typeHierarchy[h].path %>" class="text-indigo-600 hover:text-indigo-700 hover:underline font-medium"><%= pageData.typeHierarchy[h].name %></a></li>
<% } else { %>
<li class="text-slate-900 font-semibold"><%= pageData.typeHierarchy[h].name %></li>
<% } %>
<% } %>
</ol>
</nav>
<% } %>
<!-- Hero Section -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-4">
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-emerald-600 shadow-lg shadow-emerald-500/30">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
</div>
<span class="text-xs font-semibold text-emerald-600 uppercase tracking-wider"><%= pageData.isEnum ? 'Enum' : 'Data Type' %></span>
</div>
<h1 class="font-bold text-3xl text-slate-900 tracking-tight mb-3"><%= pageData.title %></h1>
<p class="text-lg text-slate-600 leading-relaxed max-w-2xl"><%- pageData.description %></p>
</div>
<!-- At a Glance Summary Box -->
<div class="mb-10 rounded-xl border border-slate-200 bg-slate-50 p-5">
<h4 class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">At a Glance</h4>
<div class="flex flex-wrap gap-x-8 gap-y-3 text-sm">
<div>
<span class="text-slate-500">Kind</span>
<span class="ml-2 font-medium text-slate-900"><%= pageData.isEnum ? 'Enum' : 'Data Type' %></span>
</div>
<% if (pageData.isEnum && pageData.valueCount > 0) { %>
<div>
<span class="text-slate-500">Values</span>
<span class="ml-2 font-medium text-slate-900"><%= pageData.valueCount %></span>
</div>
<% } else if (pageData.propertyCount > 0) { %>
<div>
<span class="text-slate-500">Properties</span>
<span class="ml-2 font-medium text-slate-900"><%= pageData.propertyCount %></span>
</div>
<% } %>
<div>
<span class="text-slate-500">JSON Format</span>
<code class="ml-2 text-xs font-mono bg-white px-2 py-0.5 rounded border border-slate-200 text-slate-700">{"_type": "<%= pageData.jsonWrapperType %>", "value": ...}</code>
</div>
</div>
</div>
<% if (pageData.isEnum) { %>
<!-- Quick Navigation for Enum Values -->
<% if (pageData.values.length >= 4) { %>
<div class="mb-6">
<div class="flex flex-wrap gap-1.5">
<% for (var qv = 0; qv < pageData.values.length; qv++) { %>
<a href="#value-<%= pageData.values[qv].value.toLowerCase().replace(/\s+/g, '-') %>" class="rounded-md bg-white px-2.5 py-1 text-xs font-medium text-slate-600 ring-1 ring-inset ring-slate-200 hover:bg-slate-50 hover:text-slate-900 transition-colors no-underline"><%= pageData.values[qv].value %></a>
<% } %>
</div>
</div>
<% } %>
<h3 id="values" class="text-base font-semibold text-slate-800 mb-4 scroll-mt-24">Possible Values</h3>
<div class="my-6 rounded-xl border border-slate-200 bg-white overflow-hidden">
<ul role="list" class="m-0 w-full list-none divide-y divide-slate-100 p-0">
<% for (var v = 0; v < pageData.values.length; v++) { %>
<li id="value-<%= pageData.values[v].value.toLowerCase().replace(/\s+/g, '-') %>" class="m-0 px-5 py-5 hover:bg-slate-50/50 transition-colors scroll-mt-24">
<dl class="m-0 flex flex-wrap items-start gap-x-3 gap-y-2">
<dt class="sr-only">Index</dt>
<dd class="text-xs font-mono text-slate-300 w-6 pt-0.5 flex-shrink-0"><%= v + 1 %>.</dd>
<dt class="sr-only">Value</dt>
<dd><code class="model-inline-code"><%= pageData.values[v].value %></code></dd>
<dt class="sr-only">Description</dt>
<dd class="w-full flex-none pl-9 text-sm text-slate-600"><%- pageData.values[v].description %></dd>
</dl>
</li>
<% } %>
</ul>
</div>
<% } else if (pageData.properties && pageData.properties.length > 0) { %>
<!-- Quick Navigation for Properties -->
<% if (pageData.properties.length >= 4) { %>
<div class="mb-6">
<div class="flex flex-wrap gap-1.5">
<% for (var qp = 0; qp < pageData.properties.length; qp++) { %>
<a href="#prop-<%= pageData.properties[qp].name %>" class="rounded-md bg-white px-2.5 py-1 text-xs font-medium text-slate-600 ring-1 ring-inset ring-slate-200 hover:bg-slate-50 hover:text-slate-900 transition-colors no-underline"><%= pageData.properties[qp].name %></a>
<% } %>
</div>
</div>
<% } %>
<h3 id="properties" class="text-base font-semibold text-slate-800 mb-4 scroll-mt-24">Properties</h3>
<div class="my-6 rounded-xl border border-slate-200 bg-white overflow-hidden">
<ul role="list" class="m-0 w-full list-none divide-y divide-slate-100 p-0">
<% for (var p = 0; p < pageData.properties.length; p++) { %>
<li id="prop-<%= pageData.properties[p].name %>" class="m-0 px-5 py-5 hover:bg-slate-50/50 transition-colors scroll-mt-24">
<div class="mb-1.5">
<code class="text-sm font-semibold text-slate-900"><%= pageData.properties[p].name %></code>
<% if (pageData.properties[p].required) { %>
<span class="ml-2 inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20">Required</span>
<% } %>
</div>
<div class="mb-2">
<span class="inline-flex items-center rounded-md bg-slate-100 px-2 py-0.5 text-xs font-mono text-slate-600">
<% if (pageData.properties[p].typeLinks && pageData.properties[p].typeLinks.length > 0) { %>
<% var typeLinks = pageData.properties[p].typeLinks; %>
<% for (var tl = 0; tl < typeLinks.length; tl++) { %>
<% if (typeLinks[tl].path) { %>
<a href="/reference/<%= typeLinks[tl].path %>" class="text-indigo-600 hover:text-indigo-700 hover:underline font-medium"><%= typeLinks[tl].label %></a>
<% } else { %>
<%= typeLinks[tl].label %>
<% } %>
<% } %>
<% } else { %>
<%= pageData.properties[p].type %>
<% } %>
</span>
</div>
<p class="m-0 text-sm text-slate-600 leading-relaxed"><%- pageData.properties[p].description %></p>
</li>
<% } %>
</ul>
</div>
<% } %>
<!-- JSON Example Section -->
<div class="border-t border-slate-200 pt-8 mt-10">
<h3 id="example" class="text-base font-semibold text-slate-800 mb-4 scroll-mt-24">JSON Example</h3>
<%- include('../partials/code', { title: "JSON", code: pageData.jsonExample, requestType: "", requestUrl: "" }) %>
</div>
<% if (pageData.title === "Permission") { %>
<div class="mt-6 rounded-lg border border-indigo-100 bg-indigo-50 p-4">
<p class="text-sm text-indigo-800 m-0">
For a complete list of all available permissions and their descriptions, see the
<a href="/reference/permissions" class="font-medium text-indigo-600 hover:text-indigo-700 hover:underline">Permissions guide</a>.
</p>
</div>
<% } %>
<!-- Related Types Section -->
<% if (pageData.relatedTypes && pageData.relatedTypes.length > 0) { %>
<div class="border-t border-slate-200 pt-8 mt-10">
<h3 id="related-types" class="text-base font-semibold text-slate-800 mb-4 scroll-mt-24">Related Types</h3>
<div class="my-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
<% for (var r = 0; r < pageData.relatedTypes.length; r++) { %>
<a href="/reference/<%= pageData.relatedTypes[r].path %>" class="group flex items-start gap-3 rounded-xl border border-slate-200 bg-white p-5 hover:border-indigo-300 hover:shadow-sm transition-all no-underline">
<div class="flex-shrink-0 mt-0.5">
<div class="flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-50 group-hover:bg-indigo-100 transition-colors">
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
</div>
</div>
<div>
<div class="text-sm font-semibold text-slate-900 group-hover:text-indigo-600 transition-colors"><%= pageData.relatedTypes[r].name %></div>
<div class="text-xs text-slate-500 mt-0.5"><%= pageData.relatedTypes[r].relationship %></div>
<% if (pageData.relatedTypes[r].description) { %>
<div class="text-xs text-slate-400 mt-1 line-clamp-2"><%= pageData.relatedTypes[r].description %></div>
<% } %>
</div>
</a>
<% } %>
</div>
</div>
<% } %>
</article>
</main>

View File

@@ -47,7 +47,13 @@
<dd><code class="model-inline-code"><%= Object.keys(pageData.columns)[i] -%></code></dd>
<dt class="sr-only">Type</dt>
<dd class="font-mono text-xs text-slate-500">
<%= pageData.columns[Object.keys(pageData.columns)[i]].type -%>
<% if(pageData.columns[Object.keys(pageData.columns)[i]].modelDocumentationPath) { %>
<a href="/reference/<%= pageData.columns[Object.keys(pageData.columns)[i]].modelDocumentationPath -%>" class="text-indigo-600 hover:text-indigo-700 hover:underline font-medium"><%= pageData.columns[Object.keys(pageData.columns)[i]].modelName -%></a>
<% } else if(pageData.columns[Object.keys(pageData.columns)[i]].typeDocumentationPath) { %>
<a href="/reference/<%= pageData.columns[Object.keys(pageData.columns)[i]].typeDocumentationPath -%>" class="text-indigo-600 hover:text-indigo-700 hover:underline font-medium"><%= pageData.columns[Object.keys(pageData.columns)[i]].type -%></a>
<% } else { %>
<%= pageData.columns[Object.keys(pageData.columns)[i]].type -%>
<% } %>
<% if(pageData.columns[Object.keys(pageData.columns)[i]].required){ %>
<span class="ml-1.5 inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20">Required</span>
<% } %>

View File

@@ -14,23 +14,49 @@
<p class="text-lg text-slate-600 leading-relaxed max-w-2xl">Your API Token needs permissions to create, update, read or delete any resource. If you do not have permissions to make a request a <code class="inline-code">4xx</code> status will be sent as response. You can manage permissions for your API Key in Project Settings > API Keys.</p>
</div>
<h2 id="consuming-webhooks" class="scroll-mt-24 text-xl font-semibold text-slate-900 mb-6 mt-12">
Permissions List
</h2>
<p class="text-slate-600 leading-relaxed mb-6">Here is a list of all the permissions:</p>
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
<ul role="list" class="m-0 divide-y divide-slate-100 p-0">
<% for(var i=0; i<pageData.permissions.length; i++) {%>
<li class="m-0 px-5 py-4 hover:bg-slate-50/50 transition-colors">
<dl class="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dd><code class="inline-code"><%= pageData.permissions[i].permission -%></code></dd>
<dd class="font-mono text-xs text-slate-500"><%= pageData.permissions[i].title -%></dd>
<dd class="w-full flex-none text-sm text-slate-600 mt-1"><%= pageData.permissions[i].description -%></dd>
</dl>
</li>
<% } %>
</ul>
<!-- Quick Navigation -->
<div class="my-6 flex gap-3 rounded-xl border border-indigo-500/20 bg-indigo-50/50 p-4">
<div class="flex-shrink-0">
<svg viewBox="0 0 16 16" aria-hidden="true" class="h-5 w-5 fill-indigo-500 stroke-white">
<circle cx="8" cy="8" r="8" stroke-width="0"></circle>
<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 7.75h1.5v3.5"></path>
<circle cx="8" cy="4" r=".5" fill="none"></circle>
</svg>
</div>
<div>
<p class="text-sm font-medium text-indigo-900 mb-2">Jump to a category</p>
<div class="flex flex-wrap gap-2">
<% for(var g=0; g<pageData.permissionGroups.length; g++) { %>
<a href="#<%= pageData.permissionGroups[g].group.toLowerCase().replace(/ /g, '-') -%>"
class="inline-flex items-center gap-1.5 rounded-md bg-white px-2.5 py-1 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-200 hover:bg-indigo-50 hover:text-indigo-700 hover:ring-indigo-200 transition-colors">
<%= pageData.permissionGroups[g].group %>
<span class="text-slate-400">(<%= pageData.permissionGroups[g].permissions.length %>)</span>
</a>
<% } %>
</div>
</div>
</div>
<% for(var g=0; g<pageData.permissionGroups.length; g++) { %>
<h2 id="<%= pageData.permissionGroups[g].group.toLowerCase().replace(/ /g, '-') -%>"
class="scroll-mt-24 text-xl font-semibold text-slate-900 mb-4 mt-12 <%= g > 0 ? 'pt-8 border-t border-slate-200' : '' %>">
<%= pageData.permissionGroups[g].group %>
<span class="ml-2 align-middle inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600"><%= pageData.permissionGroups[g].permissions.length %></span>
</h2>
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
<ul role="list" class="m-0 list-none divide-y divide-slate-100 p-0">
<% for(var i=0; i<pageData.permissionGroups[g].permissions.length; i++) { %>
<li class="m-0 px-5 py-4 hover:bg-slate-50/50 transition-colors">
<dl class="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dd><code class="inline-code"><%= pageData.permissionGroups[g].permissions[i].permission -%></code></dd>
<dd class="font-mono text-xs text-slate-500"><%= pageData.permissionGroups[g].permissions[i].title -%></dd>
<dd class="w-full flex-none text-sm text-slate-600 mt-1"><%= pageData.permissionGroups[g].permissions[i].description -%></dd>
</dl>
</li>
<% } %>
</ul>
</div>
<% } %>
</article>
</main>
</main>

View File

@@ -406,6 +406,52 @@
</ul>
</div>
</li>
<% if (typeof dataTypes !== 'undefined' && dataTypes && dataTypes.length > 0) { %>
<%
// Group data types by category
var _dtGeneral = [];
var _dtCategories = {};
var _dtCatOrder = [];
for (var _di = 0; _di < dataTypes.length; _di++) {
var _dt = dataTypes[_di];
if (_dt.category) {
if (!_dtCategories[_dt.category]) {
_dtCategories[_dt.category] = [];
_dtCatOrder.push(_dt.category);
}
_dtCategories[_dt.category].push(_dt);
} else {
_dtGeneral.push(_dt);
}
}
%>
<li class="relative mt-6">
<h6 class="text-xs font-semibold text-slate-900 uppercase tracking-wide">Data Types</h6>
<div class="relative mt-3 pl-2">
<div class="absolute inset-y-0 left-2 w-px bg-slate-200"></div>
<ul role="list" class="border-l border-transparent space-y-1">
<% for(var i=0; i<_dtGeneral.length; i++) {%>
<li class="relative"><a
class="nav-link flex justify-between gap-2 py-1.5 pr-3 text-sm transition pl-4 text-slate-600 hover:text-slate-900 -ml-px border-l-2 border-transparent hover:border-slate-300"
href="/reference/<%= _dtGeneral[i].path -%>"><span class="truncate"><%= _dtGeneral[i].name -%></span></a></li>
<% } %>
</ul>
<% for (var _ci = 0; _ci < _dtCatOrder.length; _ci++) { %>
<% var _catName = _dtCatOrder[_ci]; var _catTypes = _dtCategories[_catName]; %>
<div class="mt-3 mb-1 pl-4">
<span class="text-[10px] font-semibold text-slate-400 uppercase tracking-wider"><%= _catName %></span>
</div>
<ul role="list" class="border-l border-transparent space-y-1">
<% for(var _ti=0; _ti<_catTypes.length; _ti++) {%>
<li class="relative"><a
class="nav-link flex justify-between gap-2 py-1.5 pr-3 text-sm transition pl-4 text-slate-600 hover:text-slate-900 -ml-px border-l-2 border-transparent hover:border-slate-300"
href="/reference/<%= _catTypes[_ti].path -%>"><span class="truncate"><%= _catTypes[_ti].name -%></span></a></li>
<% } %>
</ul>
<% } %>
</div>
</li>
<% } %>
<li class="sticky bottom-0 z-10 mt-6 min-[416px]:hidden"><a
class="inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition rounded-full bg-indigo-600 py-1.5 px-4 text-white hover:bg-indigo-700 shadow-sm w-full"
href="/#">Sign in</a></li>
@@ -537,6 +583,45 @@
<% } %>
</ul>
</li>
<% if (typeof dataTypes !== 'undefined' && dataTypes && dataTypes.length > 0) { %>
<%
// Group data types by category for mobile nav
var _mGeneral = [];
var _mCategories = {};
var _mCatOrder = [];
for (var _mi = 0; _mi < dataTypes.length; _mi++) {
var _mdt = dataTypes[_mi];
if (_mdt.category) {
if (!_mCategories[_mdt.category]) {
_mCategories[_mdt.category] = [];
_mCatOrder.push(_mdt.category);
}
_mCategories[_mdt.category].push(_mdt);
} else {
_mGeneral.push(_mdt);
}
}
%>
<li class="relative">
<h6 class="text-xs font-semibold text-slate-900 uppercase tracking-wide mb-3">Data Types</h6>
<ul role="list" class="space-y-1 border-l border-slate-200 ml-2">
<% for(var i=0; i<_mGeneral.length; i++) {%>
<li><a class="mobile-nav-link block py-2 pl-4 text-sm text-slate-600 hover:text-slate-900 border-l-2 border-transparent hover:border-slate-300 -ml-px transition truncate" href="/reference/<%= _mGeneral[i].path -%>"><%= _mGeneral[i].name -%></a></li>
<% } %>
</ul>
<% for (var _mci = 0; _mci < _mCatOrder.length; _mci++) { %>
<% var _mCatName = _mCatOrder[_mci]; var _mCatTypes = _mCategories[_mCatName]; %>
<div class="mt-3 mb-1 ml-4">
<span class="text-[10px] font-semibold text-slate-400 uppercase tracking-wider"><%= _mCatName %></span>
</div>
<ul role="list" class="space-y-1 border-l border-slate-200 ml-2">
<% for(var _mti=0; _mti<_mCatTypes.length; _mti++) {%>
<li><a class="mobile-nav-link block py-2 pl-4 text-sm text-slate-600 hover:text-slate-900 border-l-2 border-transparent hover:border-slate-300 -ml-px transition truncate" href="/reference/<%= _mCatTypes[_mti].path -%>"><%= _mCatTypes[_mti].name -%></a></li>
<% } %>
</ul>
<% } %>
</li>
<% } %>
</ul>
</nav>

26
Accounts/index.d.ts vendored
View File

@@ -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";

View File

@@ -77,6 +77,7 @@
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"expo-server-sdk": "^3.15.0",
"express": "^4.21.1",
"formik": "^2.4.6",
"history": "^5.3.0",

View File

@@ -32,6 +32,7 @@ import Reseller from "Common/Models/DatabaseModels/Reseller";
import User from "Common/Models/DatabaseModels/User";
import React, { useState } from "react";
import useAsyncEffect from "use-async-effect";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
const RegisterPage: () => JSX.Element = () => {
const apiUrl: URL = SIGNUP_API_URL;
@@ -172,6 +173,36 @@ const RegisterPage: () => JSX.Element = () => {
}
}
if (!BILLING_ENABLED) {
formFields = formFields.concat([
{
overrideField: {
selfHostedCompanyName: true,
},
overrideFieldKey: "selfHostedCompanyName",
fieldType: FormFieldSchemaType.Text,
placeholder: "Acme, Inc.",
required: false,
title: "Company Name",
dataTestId: "selfHostedCompanyName",
showEvenIfPermissionDoesNotExist: true,
disableSpellCheck: true,
},
{
overrideField: {
selfHostedPhoneNumber: true,
},
overrideFieldKey: "selfHostedPhoneNumber",
fieldType: FormFieldSchemaType.Phone,
required: false,
placeholder: "+11234567890",
title: "Phone Number",
dataTestId: "selfHostedPhoneNumber",
showEvenIfPermissionDoesNotExist: true,
},
]);
}
formFields = formFields.concat([
{
field: {
@@ -206,6 +237,25 @@ const RegisterPage: () => JSX.Element = () => {
},
]);
if (!IsBillingEnabled) {
formFields = formFields.concat([
{
overrideField: {
notifySelfHosted: true,
},
overrideFieldKey: "notifySelfHosted",
fieldType: FormFieldSchemaType.Checkbox,
required: false,
defaultValue: true,
title: "Notify me about security patches and new releases",
dataTestId: "notifySelfHosted",
showEvenIfPermissionDoesNotExist: true,
spanFullRow: true,
},
]);
}
if (isCaptchaEnabled) {
formFields = formFields.concat([
{
@@ -330,6 +380,7 @@ const RegisterPage: () => JSX.Element = () => {
if (value && value.email) {
UiAnalytics.userAuth(value.email);
UiAnalytics.capture("accounts/register");
UiAnalytics.capture("sign_up");
}
LoginUtil.login({

View File

@@ -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";

View File

@@ -76,6 +76,7 @@
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"expo-server-sdk": "^3.15.0",
"express": "^4.21.1",
"formik": "^2.4.6",
"history": "^5.3.0",

View File

@@ -12,7 +12,6 @@ import Toggle from "Common/UI/Components/Toggle/Toggle";
import FieldType from "Common/UI/Components/Types/FieldType";
import { BILLING_ENABLED, getAllEnvVars } from "Common/UI/Config";
import { GetReactElementFunction } from "Common/UI/Types/FunctionTypes";
import Navigation from "Common/UI/Utils/Navigation";
import Project from "Common/Models/DatabaseModels/Project";
import User from "Common/Models/DatabaseModels/User";
import React, {
@@ -21,6 +20,7 @@ import React, {
useEffect,
useState,
} from "react";
import Navigation from "Common/UI/Utils/Navigation";
const Projects: FunctionComponent = (): ReactElement => {
const [isSubscriptionPlanYearly, setIsSubscriptionPlanYearly] =

View File

@@ -1,3 +1,4 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
@@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
modelId={modelId}
modelNameField="name"
modelType={Project}
modelAPI={AdminModelAPI}
title={"Project"}
breadcrumbLinks={[
{
@@ -41,6 +43,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
<ModelDelete
modelType={Project}
modelId={modelId}
modelAPI={AdminModelAPI}
onDeleteSuccess={() => {
Navigation.navigate(RouteMap[PageMap.PROJECTS] as Route);
}}

View File

@@ -1,3 +1,4 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import ObjectID from "Common/Types/ObjectID";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
@@ -19,6 +20,7 @@ const Projects: FunctionComponent = (): ReactElement => {
modelId={modelId}
modelNameField="name"
modelType={Project}
modelAPI={AdminModelAPI}
title={"Project"}
breadcrumbLinks={[
{
@@ -43,6 +45,7 @@ const Projects: FunctionComponent = (): ReactElement => {
<div>
<CardModelDetail<Project>
name="Project"
modelAPI={AdminModelAPI}
cardProps={{
title: "Project",
description: "Project details",

View File

@@ -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>
);
};

View File

@@ -1,3 +1,4 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
@@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
modelId={modelId}
modelNameField="email"
modelType={User}
modelAPI={AdminModelAPI}
title={"User"}
breadcrumbLinks={[
{
@@ -39,6 +41,7 @@ const DeletePage: FunctionComponent = (): ReactElement => {
<ModelDelete
modelType={User}
modelId={modelId}
modelAPI={AdminModelAPI}
onDeleteSuccess={() => {
Navigation.navigate(RouteMap[PageMap.USERS] as Route);
}}

View File

@@ -1,3 +1,4 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import ObjectID from "Common/Types/ObjectID";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
@@ -19,6 +20,7 @@ const Users: FunctionComponent = (): ReactElement => {
modelId={modelId}
modelNameField="email"
modelType={User}
modelAPI={AdminModelAPI}
title={"User"}
breadcrumbLinks={[
{
@@ -41,6 +43,7 @@ const Users: FunctionComponent = (): ReactElement => {
<div>
<CardModelDetail<User>
name="User"
modelAPI={AdminModelAPI}
cardProps={{
title: "User",
description: "User details",

View File

@@ -1,3 +1,4 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import Route from "Common/Types/API/Route";
@@ -19,6 +20,7 @@ const UserSettings: FunctionComponent = (): ReactElement => {
modelId={modelId}
modelNameField="email"
modelType={User}
modelAPI={AdminModelAPI}
title={"User"}
breadcrumbLinks={[
{
@@ -52,6 +54,7 @@ const UserSettings: FunctionComponent = (): ReactElement => {
>
<CardModelDetail<User>
name="user-master-admin-settings"
modelAPI={AdminModelAPI}
cardProps={{
title: "Master Admin Access",
description:

View File

@@ -23,6 +23,7 @@ import WhatsAppLogAPI from "./WhatsAppLogAPI";
// Import API
import ResellerPlanAPI from "Common/Server/API/ResellerPlanAPI";
import EnterpriseLicenseAPI from "Common/Server/API/EnterpriseLicenseAPI";
import OpenSourceDeploymentAPI from "Common/Server/API/OpenSourceDeploymentAPI";
import MonitorAPI from "Common/Server/API/MonitorAPI";
import ShortLinkAPI from "Common/Server/API/ShortLinkAPI";
import StatusPageAPI from "Common/Server/API/StatusPageAPI";
@@ -35,9 +36,11 @@ import UserWebAuthnAPI from "Common/Server/API/UserWebAuthnAPI";
import MonitorTest from "Common/Models/DatabaseModels/MonitorTest";
import IncidentInternalNoteAPI from "Common/Server/API/IncidentInternalNoteAPI";
import IncidentPublicNoteAPI from "Common/Server/API/IncidentPublicNoteAPI";
import IncidentEpisodePublicNoteAPI from "Common/Server/API/IncidentEpisodePublicNoteAPI";
import ScheduledMaintenanceInternalNoteAPI from "Common/Server/API/ScheduledMaintenanceInternalNoteAPI";
import ScheduledMaintenancePublicNoteAPI from "Common/Server/API/ScheduledMaintenancePublicNoteAPI";
import IncidentAPI from "Common/Server/API/IncidentAPI";
import IncidentEpisodeAPI from "Common/Server/API/IncidentEpisodeAPI";
import ScheduledMaintenanceAPI from "Common/Server/API/ScheduledMaintenanceAPI";
import AlertAPI from "Common/Server/API/AlertAPI";
// User Notification methods.
@@ -128,10 +131,48 @@ import AlertEpisodeOwnerUserService, {
import AlertEpisodeStateTimelineService, {
Service as AlertEpisodeStateTimelineServiceType,
} from "Common/Server/Services/AlertEpisodeStateTimelineService";
// IncidentEpisode Services
import IncidentEpisodeFeedService, {
Service as IncidentEpisodeFeedServiceType,
} from "Common/Server/Services/IncidentEpisodeFeedService";
import IncidentEpisodeInternalNoteService, {
Service as IncidentEpisodeInternalNoteServiceType,
} from "Common/Server/Services/IncidentEpisodeInternalNoteService";
import IncidentEpisodeMemberService, {
Service as IncidentEpisodeMemberServiceType,
} from "Common/Server/Services/IncidentEpisodeMemberService";
import IncidentEpisodeOwnerTeamService, {
Service as IncidentEpisodeOwnerTeamServiceType,
} from "Common/Server/Services/IncidentEpisodeOwnerTeamService";
import IncidentEpisodeOwnerUserService, {
Service as IncidentEpisodeOwnerUserServiceType,
} from "Common/Server/Services/IncidentEpisodeOwnerUserService";
import IncidentEpisodeStateTimelineService, {
Service as IncidentEpisodeStateTimelineServiceType,
} from "Common/Server/Services/IncidentEpisodeStateTimelineService";
import IncidentEpisodeRoleMemberService, {
Service as IncidentEpisodeRoleMemberServiceType,
} from "Common/Server/Services/IncidentEpisodeRoleMemberService";
import AlertGroupingRuleService, {
Service as AlertGroupingRuleServiceType,
} from "Common/Server/Services/AlertGroupingRuleService";
import IncidentGroupingRuleService, {
Service as IncidentGroupingRuleServiceType,
} from "Common/Server/Services/IncidentGroupingRuleService";
import IncidentSlaService, {
Service as IncidentSlaServiceType,
} from "Common/Server/Services/IncidentSlaService";
import IncidentSlaRuleService, {
Service as IncidentSlaRuleServiceType,
} from "Common/Server/Services/IncidentSlaRuleService";
import IncidentCustomFieldService, {
Service as IncidentCustomFieldServiceType,
} from "Common/Server/Services/IncidentCustomFieldService";
@@ -153,6 +194,12 @@ import IncidentOwnerUserService, {
import IncidentSeverityService, {
Service as IncidentSeverityServiceType,
} from "Common/Server/Services/IncidentSeverityService";
import IncidentRoleService, {
Service as IncidentRoleServiceType,
} from "Common/Server/Services/IncidentRoleService";
import IncidentMemberService, {
Service as IncidentMemberServiceType,
} from "Common/Server/Services/IncidentMemberService";
import IncidentStateService, {
Service as IncidentStateServiceType,
} from "Common/Server/Services/IncidentStateService";
@@ -263,6 +310,9 @@ import OnCallDutyPolicyScheduleService, {
import ProjectCallSMSConfigService, {
Service as ProjectCallSMSConfigServiceType,
} from "Common/Server/Services/ProjectCallSMSConfigService";
import ProjectUserProfileService, {
Service as ProjectUserProfileServiceType,
} from "Common/Server/Services/ProjectUserProfileService";
import ProjectSmtpConfigService, {
Service as ProjectSMTPConfigServiceType,
} from "Common/Server/Services/ProjectSmtpConfigService";
@@ -362,6 +412,9 @@ import StatusPageSSOService, {
import TeamMemberService, {
TeamMemberService as TeamMemberServiceType,
} from "Common/Server/Services/TeamMemberService";
import TeamMemberCustomFieldService, {
Service as TeamMemberCustomFieldServiceType,
} from "Common/Server/Services/TeamMemberCustomFieldService";
import TeamPermissionService, {
Service as TeamPermissionServiceType,
} from "Common/Server/Services/TeamPermissionService";
@@ -457,6 +510,18 @@ import AlertEpisodeOwnerTeam from "Common/Models/DatabaseModels/AlertEpisodeOwne
import AlertEpisodeOwnerUser from "Common/Models/DatabaseModels/AlertEpisodeOwnerUser";
import AlertEpisodeStateTimeline from "Common/Models/DatabaseModels/AlertEpisodeStateTimeline";
import AlertGroupingRule from "Common/Models/DatabaseModels/AlertGroupingRule";
import IncidentGroupingRule from "Common/Models/DatabaseModels/IncidentGroupingRule";
import IncidentSla from "Common/Models/DatabaseModels/IncidentSla";
import IncidentSlaRule from "Common/Models/DatabaseModels/IncidentSlaRule";
// IncidentEpisode Models
import IncidentEpisodeFeed from "Common/Models/DatabaseModels/IncidentEpisodeFeed";
import IncidentEpisodeInternalNote from "Common/Models/DatabaseModels/IncidentEpisodeInternalNote";
import IncidentEpisodeMember from "Common/Models/DatabaseModels/IncidentEpisodeMember";
import IncidentEpisodeOwnerTeam from "Common/Models/DatabaseModels/IncidentEpisodeOwnerTeam";
import IncidentEpisodeOwnerUser from "Common/Models/DatabaseModels/IncidentEpisodeOwnerUser";
import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline";
import IncidentEpisodeRoleMember from "Common/Models/DatabaseModels/IncidentEpisodeRoleMember";
import IncidentCustomField from "Common/Models/DatabaseModels/IncidentCustomField";
import IncidentNoteTemplate from "Common/Models/DatabaseModels/IncidentNoteTemplate";
@@ -464,6 +529,8 @@ import IncidentPostmortemTemplate from "Common/Models/DatabaseModels/IncidentPos
import IncidentOwnerTeam from "Common/Models/DatabaseModels/IncidentOwnerTeam";
import IncidentOwnerUser from "Common/Models/DatabaseModels/IncidentOwnerUser";
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
import IncidentRole from "Common/Models/DatabaseModels/IncidentRole";
import IncidentMember from "Common/Models/DatabaseModels/IncidentMember";
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline";
import IncidentTemplate from "Common/Models/DatabaseModels/IncidentTemplate";
@@ -499,6 +566,7 @@ import OnCallDutyPolicyScheduleLayer from "Common/Models/DatabaseModels/OnCallDu
import OnCallDutyPolicyScheduleLayerUser from "Common/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
import ProjectCallSMSConfig from "Common/Models/DatabaseModels/ProjectCallSMSConfig";
import ProjectSmtpConfig from "Common/Models/DatabaseModels/ProjectSmtpConfig";
import ProjectUserProfile from "Common/Models/DatabaseModels/ProjectUserProfile";
import PromoCode from "Common/Models/DatabaseModels/PromoCode";
import CodeRepository from "Common/Models/DatabaseModels/CodeRepository";
import Reseller from "Common/Models/DatabaseModels/Reseller";
@@ -527,6 +595,7 @@ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource"
import StatusPageSSO from "Common/Models/DatabaseModels/StatusPageSso";
import Team from "Common/Models/DatabaseModels/Team";
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import TeamMemberCustomField from "Common/Models/DatabaseModels/TeamMemberCustomField";
import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
import TeamComplianceSetting from "Common/Models/DatabaseModels/TeamComplianceSetting";
import TelemetryUsageBilling from "Common/Models/DatabaseModels/TelemetryUsageBilling";
@@ -1001,6 +1070,77 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
// IncidentEpisode Routes
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new IncidentEpisodeAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeFeed, IncidentEpisodeFeedServiceType>(
IncidentEpisodeFeed,
IncidentEpisodeFeedService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeInternalNote,
IncidentEpisodeInternalNoteServiceType
>(
IncidentEpisodeInternalNote,
IncidentEpisodeInternalNoteService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentEpisodeMember, IncidentEpisodeMemberServiceType>(
IncidentEpisodeMember,
IncidentEpisodeMemberService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeOwnerTeam,
IncidentEpisodeOwnerTeamServiceType
>(IncidentEpisodeOwnerTeam, IncidentEpisodeOwnerTeamService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeOwnerUser,
IncidentEpisodeOwnerUserServiceType
>(IncidentEpisodeOwnerUser, IncidentEpisodeOwnerUserService).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeStateTimeline,
IncidentEpisodeStateTimelineServiceType
>(
IncidentEpisodeStateTimeline,
IncidentEpisodeStateTimelineService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<
IncidentEpisodeRoleMember,
IncidentEpisodeRoleMemberServiceType
>(
IncidentEpisodeRoleMember,
IncidentEpisodeRoleMemberService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<AlertGroupingRule, AlertGroupingRuleServiceType>(
@@ -1009,6 +1149,32 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentGroupingRule, IncidentGroupingRuleServiceType>(
IncidentGroupingRule,
IncidentGroupingRuleService,
).getRouter(),
);
// IncidentSla
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentSla, IncidentSlaServiceType>(
IncidentSla,
IncidentSlaService,
).getRouter(),
);
// IncidentSlaRule
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentSlaRule, IncidentSlaRuleServiceType>(
IncidentSlaRule,
IncidentSlaRuleService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAnalyticsAPI<ExceptionInstance, ExceptionInstanceServiceType>(
@@ -1264,6 +1430,14 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<ProjectUserProfile, ProjectUserProfileServiceType>(
ProjectUserProfile,
ProjectUserProfileService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<MonitorGroupResource, MonitorGroupResourceServiceType>(
@@ -1280,6 +1454,14 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<TeamMemberCustomField, TeamMemberCustomFieldServiceType>(
TeamMemberCustomField,
TeamMemberCustomFieldService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<TeamPermission, TeamPermissionServiceType>(
@@ -1466,6 +1648,22 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentRole, IncidentRoleServiceType>(
IncidentRole,
IncidentRoleService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentMember, IncidentMemberServiceType>(
IncidentMember,
IncidentMemberService,
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<IncidentOwnerUser, IncidentOwnerUserServiceType>(
@@ -1810,6 +2008,10 @@ const BaseAPIFeatureSet: FeatureSet = {
`/${APP_NAME.toLocaleLowerCase()}`,
new EnterpriseLicenseAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new OpenSourceDeploymentAPI().getRouter(),
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new SlackAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
@@ -1944,6 +2146,11 @@ const BaseAPIFeatureSet: FeatureSet = {
new IncidentPublicNoteAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new IncidentEpisodePublicNoteAPI().getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new IncidentInternalNoteAPI().getRouter(),

View File

@@ -17,9 +17,11 @@ import ObjectID from "Common/Types/ObjectID";
import PositiveNumber from "Common/Types/PositiveNumber";
import DatabaseConfig from "Common/Server/DatabaseConfig";
import {
AppVersion,
EncryptionSecret,
IsBillingEnabled,
} from "Common/Server/EnvironmentConfig";
import API from "Common/Utils/API";
import AccessTokenService from "Common/Server/Services/AccessTokenService";
import EmailVerificationTokenService from "Common/Server/Services/EmailVerificationTokenService";
import MailService from "Common/Server/Services/MailService";
@@ -29,6 +31,7 @@ import UserSessionService, {
SessionMetadata,
} from "Common/Server/Services/UserSessionService";
import CookieUtil from "Common/Server/Utils/Cookie";
import JSONWebToken from "Common/Server/Utils/JsonWebToken";
import Express, {
ExpressRequest,
ExpressResponse,
@@ -54,6 +57,11 @@ const router: ExpressRouter = Express.getRouter();
const ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
interface FinalizeUserLoginResult {
sessionMetadata: SessionMetadata;
accessToken: string;
}
type FinalizeUserLoginInput = {
req: ExpressRequest;
res: ExpressResponse;
@@ -63,9 +71,9 @@ type FinalizeUserLoginInput = {
const finalizeUserLogin: (
data: FinalizeUserLoginInput,
) => Promise<SessionMetadata> = async (
) => Promise<FinalizeUserLoginResult> = async (
data: FinalizeUserLoginInput,
): Promise<SessionMetadata> => {
): Promise<FinalizeUserLoginResult> => {
const { req, res, user, isGlobalLogin } = data;
const sessionMetadata: SessionMetadata =
@@ -87,7 +95,21 @@ const finalizeUserLogin: (
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return sessionMetadata;
// Generate access token for response body (used by mobile clients)
const accessToken: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: user.id!,
email: user.email!,
name: user.name!,
timezone: user.timezone || null,
isMasterAdmin: user.isMasterAdmin!,
isGlobalLogin: isGlobalLogin,
sessionId: sessionMetadata.session.id!,
},
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return { sessionMetadata, accessToken };
};
router.post(
@@ -251,6 +273,28 @@ router.post(
logger.info("User signed up: " + savedUser.email?.toString());
if (!IsBillingEnabled && miscDataProps["notifySelfHosted"] === true) {
const instanceUrl: string = new URL(httpProtocol, host).toString();
API.post({
url: URL.fromString(
"https://oneuptime.com/api/open-source-deployment/register",
),
data: {
email: savedUser.email?.toString() || "",
name: savedUser.name?.toString() || "",
companyName:
(miscDataProps["selfHostedCompanyName"] as string) || undefined,
companyPhoneNumber:
(miscDataProps["selfHostedPhoneNumber"] as string) || undefined,
oneuptimeVersion: AppVersion,
instanceUrl: instanceUrl,
},
}).catch((err: Error) => {
logger.error(err);
});
}
return Response.sendEntityResponse(req, res, savedUser, User);
}
@@ -552,8 +596,10 @@ router.post(
next: NextFunction,
): Promise<void> => {
try {
// Try cookie first, then fallback to request body (for mobile clients)
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
(req.body.refreshToken as string | undefined);
if (!refreshToken) {
CookieUtil.removeAllCookies(req, res);
@@ -658,7 +704,26 @@ router.post(
accessTokenExpiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendEmptySuccessResponse(req, res);
// Generate access token for response body (used by mobile clients)
const newAccessToken: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: user.id!,
email: user.email!,
name: user.name!,
timezone: user.timezone || null,
isMasterAdmin: user.isMasterAdmin!,
isGlobalLogin: isGlobalLogin,
sessionId: renewedSession.session.id!,
},
expiresInSeconds: ACCESS_TOKEN_EXPIRY_SECONDS,
});
return Response.sendJsonObjectResponse(req, res, {
accessToken: newAccessToken,
refreshToken: renewedSession.refreshToken,
refreshTokenExpiresAt:
renewedSession.refreshTokenExpiresAt.toISOString(),
});
} catch (err) {
return next(err);
}
@@ -673,8 +738,10 @@ router.post(
next: NextFunction,
): Promise<void> => {
try {
// Try cookie first, then fallback to request body (for mobile clients)
const refreshToken: string | undefined =
CookieUtil.getRefreshTokenFromExpressRequest(req);
CookieUtil.getRefreshTokenFromExpressRequest(req) ||
(req.body.refreshToken as string | undefined);
if (refreshToken) {
await UserSessionService.revokeSessionByRefreshToken(refreshToken, {
@@ -987,14 +1054,21 @@ const login: LoginFunction = async (options: {
if (alreadySavedUser.password.toString() === user.password!.toString()) {
logger.info("User logged in: " + alreadySavedUser.email?.toString());
await finalizeUserLogin({
const loginResult: FinalizeUserLoginResult = await finalizeUserLogin({
req,
res,
user: alreadySavedUser,
isGlobalLogin: true,
});
return Response.sendEntityResponse(req, res, alreadySavedUser, User);
return Response.sendEntityResponse(req, res, alreadySavedUser, User, {
miscData: {
accessToken: loginResult.accessToken,
refreshToken: loginResult.sessionMetadata.refreshToken,
refreshTokenExpiresAt:
loginResult.sessionMetadata.refreshTokenExpiresAt.toISOString(),
},
});
}
}
return Response.sendErrorResponse(

View 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;

View File

@@ -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);

View File

@@ -70,6 +70,7 @@ export default class CallService {
customTwilioConfig?: TwilioConfig | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
monitorId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
@@ -144,6 +145,10 @@ export default class CallService {
callLog.alertId = options.alertId;
}
if (options.monitorId) {
callLog.monitorId = options.monitorId;
}
if (options.scheduledMaintenanceId) {
callLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}

View File

@@ -483,6 +483,7 @@ export default class MailService {
timeout?: number | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
monitorId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
@@ -516,6 +517,10 @@ export default class MailService {
emailLog.alertId = options.alertId;
}
if (options.monitorId) {
emailLog.monitorId = options.monitorId;
}
if (options.scheduledMaintenanceId) {
emailLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}

View File

@@ -33,6 +33,7 @@ export default class SmsService {
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
monitorId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
@@ -91,6 +92,10 @@ export default class SmsService {
smsLog.alertId = options.alertId;
}
if (options.monitorId) {
smsLog.monitorId = options.monitorId;
}
if (options.scheduledMaintenanceId) {
smsLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}

View File

@@ -41,6 +41,7 @@ export default class WhatsAppService {
userOnCallLogTimelineId?: ObjectID | undefined;
incidentId?: ObjectID | undefined;
alertId?: ObjectID | undefined;
monitorId?: ObjectID | undefined;
scheduledMaintenanceId?: ObjectID | undefined;
statusPageId?: ObjectID | undefined;
statusPageAnnouncementId?: ObjectID | undefined;
@@ -96,6 +97,10 @@ export default class WhatsAppService {
whatsAppLog.alertId = options.alertId;
}
if (options.monitorId) {
whatsAppLog.monitorId = options.monitorId;
}
if (options.scheduledMaintenanceId) {
whatsAppLog.scheduledMaintenanceId = options.scheduledMaintenanceId;
}

View File

@@ -0,0 +1,66 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " incidentEpisodeTitle) }}
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident Episode Title:" text=incidentEpisodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentEpisodeSeverity }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=incidentEpisodeDescription }}
{{> DetailBoxEnd this }}
{{#if incidentsList}}
{{> TitleBlock title=(concat "Incidents in this Episode (" incidentsCount ")") }}
<!-- Incidents List Container -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td style="border: 0; margin: 0; padding: 0;">
{{{incidentsList}}}
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<!-- /Incidents List Container -->
{{/if}}
{{> InfoBlock info="ACTION REQUIRED: Please acknowledge this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=acknowledgeIncidentEpisodeLink buttonText="Acknowledge Incident Episode"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=acknowledgeIncidentEpisodeLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> TitleBlock title="Why am I receiving this email?"}}
{{> InfoBlock info="You are receiving this email because you are a member of the team that is responsible for this incident episode or you are currently on-call."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,30 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="You have been added as the owner of this incident episode."}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{> DetailBoxField title="Description: " text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,37 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="A new note has been posted on this incident episode."}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{#if isPrivateNote}}
{{> DetailBoxField title="Private Note: " text=note }}
{{else}}
{{> DetailBoxField title="Public Note: " text=note }}
{{/if}}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,35 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info=(concat "A new incident episode has been created in the project - " projectName)}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Episode Created By: " text=declaredBy }}
{{> DetailBoxField title="Episode Created At: " text=declaredAt }}
{{> DetailBoxField title="Severity: " text=episodeSeverity }}
{{> DetailBoxField title="Description: " text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,37 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident Episode " episodeNumber ": " episodeTitle) }}
{{> InfoBlock info="Incident episode state has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> StateTransition this}}
{{#ifNotCond previousStateDurationText ""}}
{{> DetailBoxField title="Duration in Previous State:" text=previousStateDurationText }}
{{/ifNotCond}}
{{> DetailBoxField title="Episode Title:" text=episodeTitle }}
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
{{> DetailBoxField title="Severity:" text=episodeSeverity }}
{{> DetailBoxField title="Description:" text=episodeDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident episode by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=episodeViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=episodeViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident episode changes."}}
{{> OwnerInfo this }}
{{> UnsubscribeOwnerEmail this }}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,32 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title=(concat "Incident " incidentNumber ": " incidentTitle) }}
{{> InfoBlock info=(concat "You have been assigned as " incidentRole " to this incident.")}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident Title:" text=incidentTitle }}
{{> DetailBoxField title="Your Role: " text=incidentRole }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Description: " text=incidentDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info="You can view this incident by clicking on the button below - "}}
{{> ButtonBlock buttonUrl=incidentViewLink buttonText="View on Dashboard"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info=incidentViewLink}}
{{> InfoBlock info="You will be notified when the status of this incident changes."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,37 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title="Invoice from OneUptime" }}
{{> InfoBlock info="A new invoice has been generated for your account. Here are the details:"}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Invoice Number:" text=invoiceNumber }}
{{> DetailBoxField title="Invoice Date:" text=invoiceDate }}
{{> DetailBoxField title="Amount:" text=amount }}
{{#if description}}
{{> DetailBoxField title="Description:" text=description }}
{{/if}}
{{> DetailBoxEnd this }}
{{#if invoicePdfUrl}}
{{> InfoBlock info="You can view and download your invoice by clicking the button below:"}}
{{> ButtonBlock buttonUrl=invoicePdfUrl buttonText="View Invoice PDF"}}
{{> InfoBlock info="Or copy and paste this link:"}}
{{> InfoBlock info=invoicePdfUrl}}
{{/if}}
{{#if dashboardLink}}
{{> InfoBlock info="You can also view all your invoices in your dashboard:"}}
{{> ButtonBlock buttonUrl=dashboardLink buttonText="View Billing Dashboard"}}
{{/if}}
{{> InfoBlock info="You have received this email because you are subscribed to receive invoice notifications for this project."}}
{{> Footer this }}
{{> End this}}

View File

@@ -0,0 +1,36 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "New Incident: " episodeTitle) }}
{{> InfoBlock info="A new incident has been reported that may affect the services you're subscribed to."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident" text=episodeTitle }}
{{#if episodeSeverity}}
{{> DetailBoxField title="Severity" text=episodeSeverity }}
{{/if}}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{#if episodeDescription}}
{{> DetailBoxField title="Description" text=episodeDescription }}
{{/if}}
{{> DetailBoxEnd this }}
{{#if detailsUrl}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
{{else}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> Footer this}}
{{> End this}}

View File

@@ -0,0 +1,30 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Incident: " episodeTitle) }}
{{> InfoBlock info="A new note has been added to the incident. Here are the details:"}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident Title" text=episodeTitle }}
{{> DetailBoxField title="Resources Affected" text=resourcesAffected }}
{{#if episodeSeverity}}
{{> DetailBoxField title="Severity" text=episodeSeverity }}
{{/if}}
{{> DetailBoxField title="Note" text=note }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{> End this}}

View File

@@ -0,0 +1,34 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=emailTitle }}
{{> InfoBlock info="The status of an incident affecting services you're subscribed to has been updated."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Incident" text=episodeTitle }}
{{> DetailBoxField title="Current State" text=episodeState }}
{{#if episodeSeverity}}
{{> DetailBoxField title="Severity" text=episodeSeverity }}
{{/if}}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{> DetailBoxEnd this }}
{{#if detailsUrl}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
{{else}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> Footer this}}
{{> End this}}

81
App/package-lock.json generated
View File

@@ -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",
@@ -83,6 +84,7 @@
"ejs": "^3.1.10",
"elkjs": "^0.10.0",
"esbuild": "^0.25.5",
"expo-server-sdk": "^3.15.0",
"express": "^4.21.1",
"formik": "^2.4.6",
"history": "^5.3.0",
@@ -1609,13 +1611,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -2165,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",
@@ -2298,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",
@@ -2388,9 +2410,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -4166,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",
@@ -4191,9 +4232,9 @@
"dev": true
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -4289,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",
@@ -4800,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",

View File

@@ -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",

View 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);
}
});
}

View 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);
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
});
});
});

View 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);
});
});
});

View 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");
});
});
});

View 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
View 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");
});
});

View 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");
});
});
});

View 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",
);
});
});
});
});

View 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;
}
});
});
});
});

View 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
View 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;
}

View 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
View 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

File diff suppressed because it is too large Load Diff

41
CLI/package.json Normal file
View 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
View 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"]
}

View File

@@ -77,6 +77,7 @@ export enum AIAgentConnectionStatus {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -99,7 +100,11 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -126,7 +131,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -154,7 +159,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -176,7 +181,7 @@ export default class AIAgent extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [],
})
@TableColumn({
@@ -202,7 +207,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -231,6 +236,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -258,6 +264,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -299,6 +306,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -328,7 +336,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [],
})
@TableColumn({
@@ -358,7 +366,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [],
})
@TableColumn({
@@ -421,7 +429,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.ReadAllProjectResources],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@@ -446,7 +454,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.ReadAllProjectResources],
update: [],
})
@TableColumn({
@@ -490,6 +498,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -515,7 +524,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectMember,
Permission.CreateProjectAIAgent,
],
read: [Permission.Public],
read: [Permission.Public, Permission.ReadAllProjectResources],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
@@ -552,6 +561,7 @@ export default class AIAgent extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgent,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,

View File

@@ -41,6 +41,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -85,6 +86,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -122,6 +124,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -153,6 +156,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -191,6 +195,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -222,6 +227,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -259,6 +265,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -290,6 +297,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -328,6 +336,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -352,6 +361,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -386,6 +396,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -410,6 +421,7 @@ export default class AIAgentOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -40,6 +40,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -84,6 +85,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -121,6 +123,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -152,6 +155,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -190,6 +194,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -221,6 +226,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -258,6 +264,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -289,6 +296,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -327,6 +335,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -351,6 +360,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -385,6 +395,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -409,6 +420,7 @@ export default class AIAgentOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAIAgentOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -55,6 +55,7 @@ import { AIAgentTaskMetadata } from "../../Types/AI/AIAgentTaskMetadata";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -82,6 +83,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -120,6 +122,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -150,6 +153,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -183,6 +187,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -216,6 +221,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -258,6 +264,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -293,6 +300,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -323,6 +331,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -354,6 +363,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -387,6 +397,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -415,6 +426,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -438,6 +450,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -495,7 +508,11 @@ export default class AIAgentTask extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@@ -515,7 +532,11 @@ export default class AIAgentTask extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
@@ -543,6 +564,7 @@ export default class AIAgentTask extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -53,6 +53,7 @@ import LogSeverity from "../../Types/Log/LogSeverity";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -80,6 +81,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -118,6 +120,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -148,6 +151,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -186,6 +190,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -216,6 +221,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -254,6 +260,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -284,6 +291,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -318,6 +326,7 @@ export default class AIAgentTaskLog extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -379,7 +388,11 @@ export default class AIAgentTaskLog extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@@ -399,7 +412,11 @@ export default class AIAgentTaskLog extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({

View File

@@ -55,6 +55,7 @@ import EnableDocumentation from "../../Types/Database/EnableDocumentation";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -82,6 +83,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -120,6 +122,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -150,6 +153,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -188,6 +192,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -218,6 +223,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -256,6 +262,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -286,6 +293,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -324,6 +332,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -354,6 +363,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -388,6 +398,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -420,6 +431,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -454,6 +466,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -483,6 +496,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -512,6 +526,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -548,6 +563,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -577,6 +593,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -606,6 +623,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -635,6 +653,7 @@ export default class AIAgentTaskPullRequest extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTask,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -693,7 +712,11 @@ export default class AIAgentTaskPullRequest extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@@ -713,7 +736,11 @@ export default class AIAgentTaskPullRequest extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({

View File

@@ -40,6 +40,7 @@ import TelemetryException from "./TelemetryException";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -79,6 +80,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -115,6 +117,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -145,6 +148,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -181,6 +185,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -211,6 +216,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -247,6 +253,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -277,6 +284,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -314,6 +322,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -337,6 +346,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -370,6 +380,7 @@ export default class AIAgentTaskTelemetryException extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectAIAgentTaskTelemetryException,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -57,6 +57,7 @@ import NotificationRuleWorkspaceChannel from "../../Types/Workspace/Notification
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -106,6 +107,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -142,6 +144,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -173,6 +176,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -209,6 +213,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -244,6 +249,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -281,6 +287,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -357,6 +364,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -399,6 +407,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -409,9 +418,8 @@ export default class Alert extends BaseModel {
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
title: "Monitor ID",
description: "ID of the monitor this alert belongs to",
})
@Column({
type: ColumnType.ObjectID,
@@ -432,6 +440,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -443,9 +452,9 @@ export default class Alert extends BaseModel {
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Monitor,
modelType: OnCallDutyPolicy,
title: "On-Call Duty Policies",
description: "List of on-call duty policy affected by this alert.",
description: "List of on-call duty policies affected by this alert.",
})
@ManyToMany(
() => {
@@ -456,15 +465,15 @@ export default class Alert extends BaseModel {
@JoinTable({
name: "AlertOnCallDutyPolicy",
inverseJoinColumn: {
name: "monitorId",
referencedColumnName: "_id",
},
joinColumn: {
name: "onCallDutyPolicyId",
referencedColumnName: "_id",
},
joinColumn: {
name: "alertId",
referencedColumnName: "_id",
},
})
public onCallDutyPolicies?: Array<OnCallDutyPolicy> = undefined; // monitors affected by this alert.
public onCallDutyPolicies?: Array<OnCallDutyPolicy> = undefined; // on-call duty policies affected by this alert.
@ColumnAccessControl({
create: [
@@ -478,6 +487,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -525,6 +535,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -566,6 +577,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -579,13 +591,13 @@ export default class Alert extends BaseModel {
type: TableColumnType.ObjectID,
required: true,
isDefaultValueColumn: true,
canReadOnRelationQuery: true,
title: "Current Alert State ID",
description: "Current Alert State ID",
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
@@ -603,6 +615,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -644,6 +657,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -679,13 +693,14 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "monitorStatusWhenThisAlertWasCreatedId",
type: TableColumnType.Entity,
modelType: AlertState,
modelType: MonitorStatus,
title: "Monitor status when this alert was created",
description: "Monitor status when this alert was created",
})
@@ -714,6 +729,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -748,6 +764,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -776,6 +793,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -809,6 +827,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -834,6 +853,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -841,6 +861,7 @@ export default class Alert extends BaseModel {
isDefaultValueColumn: false,
required: false,
type: TableColumnType.JSON,
computed: true,
})
@Column({
type: ColumnType.JSON,
@@ -856,6 +877,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -881,6 +903,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -913,6 +936,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -939,6 +963,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -968,6 +993,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1002,6 +1028,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1035,6 +1062,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1055,6 +1083,33 @@ export default class Alert extends BaseModel {
})
public alertNumber?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.ShortText,
title: "Alert Number With Prefix",
description: "Alert number with prefix (e.g., 'ALT-42' or '#42')",
computed: true,
canReadOnRelationQuery: true,
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public alertNumberWithPrefix?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
@@ -1086,6 +1141,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1127,6 +1183,7 @@ export default class Alert extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlert,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,

View File

@@ -40,6 +40,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -75,6 +76,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -110,6 +112,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -140,6 +143,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -174,6 +178,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -208,6 +213,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -236,6 +242,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -272,6 +279,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -296,6 +304,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -329,6 +338,7 @@ export default class AlertCustomField extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertCustomField,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -53,6 +53,7 @@ import NotificationRuleWorkspaceChannel from "../../Types/Workspace/Notification
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -103,6 +104,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -139,6 +141,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -169,6 +172,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -204,6 +208,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -237,6 +242,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -254,6 +260,32 @@ export default class AlertEpisode extends BaseModel {
})
public episodeNumber?: number = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
required: false,
type: TableColumnType.ShortText,
title: "Episode Number With Prefix",
description: "Episode number with prefix (e.g., 'AE-42' or '#42')",
computed: true,
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
})
public episodeNumberWithPrefix?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@@ -266,6 +298,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -307,6 +340,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -342,6 +376,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -383,6 +418,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -417,6 +453,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -450,6 +487,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -483,6 +521,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -504,6 +543,31 @@ export default class AlertEpisode extends BaseModel {
})
public resolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.Date,
title: "All Alerts Resolved At",
description:
"When all alerts in this episode were first detected as resolved. Used for resolve delay calculation.",
})
@Column({
type: ColumnType.Date,
nullable: true,
unique: false,
})
public allAlertsResolvedAt?: Date = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@@ -516,6 +580,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -557,6 +622,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -591,6 +657,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -632,6 +699,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -666,6 +734,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -708,6 +777,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -742,6 +812,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -783,6 +854,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -810,6 +882,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -818,6 +891,7 @@ export default class AlertEpisode extends BaseModel {
type: TableColumnType.Number,
required: true,
isDefaultValueColumn: true,
computed: true,
title: "Alert Count",
description: "Denormalized count of alerts in this episode",
defaultValue: 0,
@@ -841,6 +915,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -869,6 +944,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -897,6 +973,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -929,6 +1006,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -976,6 +1054,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1013,6 +1092,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1082,6 +1162,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1113,6 +1194,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1143,6 +1225,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1176,6 +1259,7 @@ export default class AlertEpisode extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisode,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,

View File

@@ -55,6 +55,7 @@ export enum AlertEpisodeFeedEventType {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
delete: [],
update: [],
@@ -90,6 +91,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -126,6 +128,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -156,6 +159,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -192,6 +196,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -221,6 +226,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -258,6 +264,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -332,6 +339,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -360,6 +368,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -388,6 +397,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -416,6 +426,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -447,6 +458,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -484,6 +496,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -512,6 +525,7 @@ export default class AlertEpisodeFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeFeed,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -43,6 +43,7 @@ import {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -87,6 +88,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -123,6 +125,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -153,6 +156,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -189,6 +193,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -218,6 +223,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -255,6 +261,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -329,6 +336,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -361,6 +369,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -404,6 +413,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -435,6 +445,7 @@ export default class AlertEpisodeInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -41,6 +41,7 @@ export enum AlertEpisodeMemberAddedBy {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -86,6 +87,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -122,6 +124,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -152,6 +155,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -188,6 +192,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -218,6 +223,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -254,6 +260,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -284,6 +291,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -312,6 +320,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -344,6 +353,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -381,6 +391,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -410,6 +421,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -446,6 +458,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -475,6 +488,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -512,6 +526,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -535,6 +550,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -568,6 +584,7 @@ export default class AlertEpisodeMember extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeMember,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -33,6 +33,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -78,6 +79,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -114,6 +116,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -144,6 +147,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -181,6 +185,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -211,6 +216,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -248,6 +254,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -279,6 +286,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -316,6 +324,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -339,6 +348,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -372,6 +382,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -400,6 +411,7 @@ export default class AlertEpisodeOwnerTeam extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerTeam,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -32,6 +32,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -77,6 +78,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -113,6 +115,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -143,6 +146,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -179,6 +183,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -209,6 +214,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -246,6 +252,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -277,6 +284,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -314,6 +322,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -337,6 +346,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -370,6 +380,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -398,6 +409,7 @@ export default class AlertEpisodeOwnerUser extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeOwnerUser,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -36,6 +36,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -82,6 +83,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -118,6 +120,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -148,6 +151,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -184,6 +188,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -213,6 +218,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -250,6 +256,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -324,6 +331,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -365,6 +373,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -395,6 +404,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -421,6 +431,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -428,6 +439,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
isDefaultValueColumn: false,
required: false,
type: TableColumnType.JSON,
computed: true,
})
@Column({
type: ColumnType.JSON,
@@ -448,6 +460,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -478,6 +491,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -506,6 +520,7 @@ export default class AlertEpisodeStateTimeline extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertEpisodeStateTimeline,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -57,6 +57,7 @@ export enum AlertFeedEventType {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
delete: [],
update: [],
@@ -92,6 +93,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -128,6 +130,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -159,6 +162,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -195,6 +199,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -225,6 +230,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -262,6 +268,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -337,6 +344,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -367,6 +375,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -395,6 +404,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -424,6 +434,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -456,6 +467,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -493,6 +505,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -522,6 +535,7 @@ export default class AlertFeed extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertFeed,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -62,6 +62,7 @@ export interface AlertGroupingRuleGroupByFields {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -104,6 +105,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -139,6 +141,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -168,6 +171,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -201,6 +205,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -232,6 +237,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -267,6 +273,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -301,6 +308,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -334,6 +342,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -379,6 +388,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -424,6 +434,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -469,6 +480,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -514,6 +526,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -546,6 +559,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -578,6 +592,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -610,6 +625,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -644,6 +660,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -657,13 +674,13 @@ export default class AlertGroupingRule extends BaseModel {
title: "Group By Monitor",
description:
"When enabled, alerts from different monitors will be grouped into separate episodes. When disabled, alerts from any monitor can be grouped together.",
defaultValue: true,
defaultValue: false,
isDefaultValueColumn: true,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
default: true,
default: false,
})
public groupByMonitor?: boolean = undefined;
@@ -678,6 +695,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -712,6 +730,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -746,6 +765,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -780,6 +800,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -814,6 +835,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -848,6 +870,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -879,6 +902,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -910,6 +934,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -941,6 +966,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -975,6 +1001,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1009,6 +1036,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1043,6 +1071,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1077,6 +1106,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1111,6 +1141,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1145,6 +1176,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1190,6 +1222,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1229,6 +1262,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1261,6 +1295,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1300,6 +1335,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -1321,6 +1357,8 @@ export default class AlertGroupingRule extends BaseModel {
})
public defaultAssignToTeamId?: ObjectID = undefined;
// Episode Configuration Fields
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
@@ -1332,6 +1370,145 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditAlertGroupingRule,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Label,
title: "Episode Labels",
description:
"Labels to automatically apply to episodes created by this rule.",
})
@ManyToMany(
() => {
return Label;
},
{ eager: false },
)
@JoinTable({
name: "AlertGroupingRuleEpisodeLabel",
inverseJoinColumn: {
name: "labelId",
referencedColumnName: "_id",
},
joinColumn: {
name: "alertGroupingRuleId",
referencedColumnName: "_id",
},
})
public episodeLabels?: Array<Label> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.CreateAlertGroupingRule,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditAlertGroupingRule,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: User,
title: "Episode Owner Users",
description:
"Users to automatically add as owners to episodes created by this rule.",
})
@ManyToMany(
() => {
return User;
},
{ eager: false },
)
@JoinTable({
name: "AlertGroupingRuleEpisodeOwnerUser",
inverseJoinColumn: {
name: "userId",
referencedColumnName: "_id",
},
joinColumn: {
name: "alertGroupingRuleId",
referencedColumnName: "_id",
},
})
public episodeOwnerUsers?: Array<User> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.CreateAlertGroupingRule,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.EditAlertGroupingRule,
],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Team,
title: "Episode Owner Teams",
description:
"Teams to automatically add as owners to episodes created by this rule.",
})
@ManyToMany(
() => {
return Team;
},
{ eager: false },
)
@JoinTable({
name: "AlertGroupingRuleEpisodeOwnerTeam",
inverseJoinColumn: {
name: "teamId",
referencedColumnName: "_id",
},
joinColumn: {
name: "alertGroupingRuleId",
referencedColumnName: "_id",
},
})
public episodeOwnerTeams?: Array<Team> = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.CreateAlertGroupingRule,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -1368,6 +1545,7 @@ export default class AlertGroupingRule extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertGroupingRule,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -43,6 +43,7 @@ import {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -87,6 +88,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -123,6 +125,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -154,6 +157,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -190,6 +194,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -220,6 +225,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -257,6 +263,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -333,6 +340,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -367,6 +375,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -410,6 +419,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -443,6 +453,7 @@ export default class AlertInternalNote extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertInternalNote,
Permission.ReadAllProjectResources,
],
update: [],
})

View File

@@ -40,6 +40,7 @@ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
delete: [
Permission.ProjectOwner,
@@ -84,6 +85,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -121,6 +123,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -152,6 +155,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -188,6 +192,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -223,6 +228,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [
Permission.ProjectOwner,
@@ -259,6 +265,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [],
})
@@ -297,6 +304,7 @@ export default class AlertNoteTemplate extends BaseModel {
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadAlertNoteTemplate,
Permission.ReadAllProjectResources,
],
update: [],
})

Some files were not shown because too many files have changed in this diff Show More