diff --git a/src/config/IAppConfig.ts b/src/config/IAppConfig.ts index d24467c48b8..adfcd5d5d60 100644 --- a/src/config/IAppConfig.ts +++ b/src/config/IAppConfig.ts @@ -190,4 +190,6 @@ export interface IServerConfig { clickhouse_mode: boolean; download_custom_buttons_json: string; feature_study_export: boolean; + uptime_robot_status_page_url: string | null; + uptime_robot_api_key: string | null; } diff --git a/src/config/serverConfigDefaults.ts b/src/config/serverConfigDefaults.ts index 0857a1d7324..630f998eaf6 100644 --- a/src/config/serverConfigDefaults.ts +++ b/src/config/serverConfigDefaults.ts @@ -248,6 +248,9 @@ export const ServerConfigDefaults: Partial = { download_custom_buttons_json: '', feature_study_export: false, + + uptime_robot_status_page_url: null, + uptime_robot_api_key: null, }; export default ServerConfigDefaults; diff --git a/src/shared/components/userMessager/UserMessage.tsx b/src/shared/components/userMessager/UserMessage.tsx index f922d2f4c1b..7a75c7e28db 100644 --- a/src/shared/components/userMessager/UserMessage.tsx +++ b/src/shared/components/userMessager/UserMessage.tsx @@ -10,6 +10,7 @@ import { Portal } from 'react-portal'; import { getBrowserWindow, MobxPromise } from 'cbioportal-frontend-commons'; import ExtendedRouterStore from 'shared/lib/ExtendedRouterStore'; import { getServerConfig } from 'config/config'; +import { STATUS_PROVIDERS } from 'shared/lib/statusProviders'; export interface IUserMessage { dateStart?: number; @@ -17,6 +18,7 @@ export interface IUserMessage { content: string | JSX.Element; id: string; showCondition?: (routingStore: ExtendedRouterStore) => void; + backgroundColor?: string; } function makeMessageKey(id: string) { @@ -77,11 +79,36 @@ export default class UserMessager extends React.Component< super(props); makeObservable(this); this.messageData = remoteData(async () => { - return Promise.resolve(props.messages || MESSAGE_DATA); + const staticMessages = props.messages || MESSAGE_DATA || []; + + // Fetch messages from all status providers + const statusMessages = await this.fetchStatusProviderMessages(); + + // Combine status provider messages with static messages + // Status provider messages take priority (appear first) + return [...statusMessages, ...staticMessages]; }); } private messageData: MobxPromise; + /** + * Fetches messages from all registered status providers + */ + private async fetchStatusProviderMessages(): Promise { + try { + // Fetch from all providers in parallel + const providerResults = await Promise.all( + STATUS_PROVIDERS.map(provider => provider.fetchMessages()) + ); + + // Flatten the results into a single array + return _.flatten(providerResults); + } catch (error) { + console.error('Error fetching status provider messages:', error); + return []; + } + } + @observable dismissed = false; get shownMessage() { @@ -142,12 +169,21 @@ export default class UserMessager extends React.Component< this.messageData.isComplete && this.shownMessage ) { + const bannerStyle: React.CSSProperties = this.shownMessage + .backgroundColor + ? { backgroundColor: this.shownMessage.backgroundColor } + : {}; + return ( -
+
; +} diff --git a/src/shared/lib/statusProviders/UptimeRobotStatusProvider.tsx b/src/shared/lib/statusProviders/UptimeRobotStatusProvider.tsx new file mode 100644 index 00000000000..1108898f2a2 --- /dev/null +++ b/src/shared/lib/statusProviders/UptimeRobotStatusProvider.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { IStatusProvider } from './IStatusProvider'; +import { IUserMessage } from 'shared/components/userMessager/UserMessage'; +import { + fetchUptimeRobotEvents, + getActiveEvents, + getEventSeverityColor, + UptimeRobotEvent, +} from './uptimeRobot'; + +/** + * Status provider for UptimeRobot integration. + * Fetches events from UptimeRobot status page and converts them to banner messages. + */ +export class UptimeRobotStatusProvider implements IStatusProvider { + async fetchMessages(): Promise { + try { + const eventFeed = await fetchUptimeRobotEvents(); + if (!eventFeed || !eventFeed.results) { + return []; + } + + const activeEvents = getActiveEvents(eventFeed.results); + const statusPageUrl = eventFeed.statusPageUrl; + + return activeEvents.map((event: UptimeRobotEvent) => { + const endTime = event.endDateGMT + ? new Date(event.endDateGMT).getTime() + : Date.now() + 24 * 60 * 60 * 1000; // Default to 24 hours from now + + return { + id: `uptime-robot-${event.id}`, + dateEnd: endTime, + content: ( +
+ {event.title} + {event.description && ( + <> + {': '} + {event.description} + + )} + {statusPageUrl && ( + <> + {' '} + + View status page + + + )} +
+ ), + backgroundColor: getEventSeverityColor(event.icon), + }; + }); + } catch (error) { + console.error('Error fetching UptimeRobot messages:', error); + return []; + } + } +} diff --git a/src/shared/lib/statusProviders/index.ts b/src/shared/lib/statusProviders/index.ts new file mode 100644 index 00000000000..a9930186e66 --- /dev/null +++ b/src/shared/lib/statusProviders/index.ts @@ -0,0 +1,16 @@ +import { IStatusProvider } from './IStatusProvider'; +import { UptimeRobotStatusProvider } from './UptimeRobotStatusProvider'; + +/** + * Registry of all status page providers. + * Add new status providers here to enable them. + * + * Example: To add StatusPage.io integration: + * 1. Create StatusPageIoProvider.ts that implements IStatusProvider + * 2. Import it: import { StatusPageIoProvider } from './StatusPageIoProvider'; + * 3. Add to array: new StatusPageIoProvider(), + */ +export const STATUS_PROVIDERS: IStatusProvider[] = [ + new UptimeRobotStatusProvider(), + // Add additional status providers here +]; diff --git a/src/shared/lib/statusProviders/uptimeRobot.ts b/src/shared/lib/statusProviders/uptimeRobot.ts new file mode 100644 index 00000000000..b90adb317a0 --- /dev/null +++ b/src/shared/lib/statusProviders/uptimeRobot.ts @@ -0,0 +1,115 @@ +import { getServerConfig } from 'config/config'; + +export interface UptimeRobotEvent { + type: 'announcement' | 'update'; + eventType: string; + id: number; + title: string; + content: string; + description: string; + date: string; + time: string; + timeGMT: string; + endDate?: string; + endDateGMT?: string; + timestamp: number; + status: number; // 1 = resolved, 2 = active/ongoing + icon: string; +} + +export interface UptimeRobotEventFeedResponse { + status: boolean; + results: UptimeRobotEvent[]; + meta: { + count: number; + date_range: { + from: string; + to: string; + }; + }; + statusPageUrl?: string; // Added to track the status page URL +} + +/** + * Fetches the event feed from UptimeRobot status page + * @returns Promise resolving to the event feed response + */ +export async function fetchUptimeRobotEvents(): Promise { + const config = getServerConfig(); + + // Check if UptimeRobot is configured (both URL and API key required) + if (!config.uptime_robot_status_page_url || !config.uptime_robot_api_key) { + return null; + } + + try { + const url = `${config.uptime_robot_status_page_url}/api/getEventFeed/${config.uptime_robot_api_key}`; + const response = await fetch(url); + + if (!response.ok) { + console.error( + 'Failed to fetch UptimeRobot events:', + response.statusText + ); + return null; + } + + const data: UptimeRobotEventFeedResponse = await response.json(); + // Add the status page URL to the response so we can link to it + data.statusPageUrl = config.uptime_robot_status_page_url; + return data; + } catch (error) { + console.error('Error fetching UptimeRobot events:', error); + return null; + } +} + +/** + * Filters events to return only active/ongoing events + * @param events Array of UptimeRobot events + * @returns Array of active events + */ +export function getActiveEvents( + events: UptimeRobotEvent[] +): UptimeRobotEvent[] { + const now = Date.now(); + + return events.filter(event => { + // Status 2 indicates active/ongoing event + const isActive = event.status === 2; + + // Check if event is still within its time window + const startTime = event.timestamp * 1000; // Convert to milliseconds + const hasStarted = startTime <= now; + + // If there's an end date, check if it hasn't passed yet + let notEnded = true; + if (event.endDateGMT) { + const endTime = new Date(event.endDateGMT).getTime(); + notEnded = endTime > now; + } + + return isActive && hasStarted && notEnded; + }); +} + +/** + * Converts UptimeRobot event severity icon to appropriate CSS class/color + * @param icon The icon name from UptimeRobot + * @returns CSS color for the banner + */ +export function getEventSeverityColor(icon: string): string { + switch (icon) { + case 'alert-triangle': + case 'alert-octagon': + return '#d9534f'; // Red/danger + case 'alert-circle': + return '#f0ad4e'; // Orange/warning + case 'info': + return '#5bc0de'; // Blue/info + case 'check-circle': + return '#5cb85c'; // Green/success + default: + return '#f0ad4e'; // Default to warning + } +}