Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config/IAppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/config/serverConfigDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ export const ServerConfigDefaults: Partial<IServerConfig> = {
download_custom_buttons_json: '',

feature_study_export: false,

uptime_robot_status_page_url: null,
uptime_robot_api_key: null,
};

export default ServerConfigDefaults;
40 changes: 38 additions & 2 deletions src/shared/components/userMessager/UserMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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;
dateEnd: number;
content: string | JSX.Element;
id: string;
showCondition?: (routingStore: ExtendedRouterStore) => void;
backgroundColor?: string;
}

function makeMessageKey(id: string) {
Expand Down Expand Up @@ -77,11 +79,36 @@ export default class UserMessager extends React.Component<
super(props);
makeObservable(this);
this.messageData = remoteData<IUserMessage[]>(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<IUserMessage[]>;

/**
* Fetches messages from all registered status providers
*/
private async fetchStatusProviderMessages(): Promise<IUserMessage[]> {
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() {
Expand Down Expand Up @@ -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 (
<Portal
isOpened={true}
node={document.getElementById('pageTopContainer')}
>
<div className={styles.messager} onClick={this.handleClick}>
<div
className={styles.messager}
onClick={this.handleClick}
style={bannerStyle}
>
<i
className={classNames(
styles.close,
Expand Down
14 changes: 14 additions & 0 deletions src/shared/lib/statusProviders/IStatusProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IUserMessage } from 'shared/components/userMessager/UserMessage';

/**
* Interface for status page integrations.
* Each status provider (UptimeRobot, StatusPage.io, etc.) implements this interface
* to provide status messages that will be displayed as banners.
*/
export interface IStatusProvider {
/**
* Fetches active status messages from the provider.
* @returns Promise resolving to an array of user messages
*/
fetchMessages(): Promise<IUserMessage[]>;
}
65 changes: 65 additions & 0 deletions src/shared/lib/statusProviders/UptimeRobotStatusProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<IUserMessage[]> {
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: (
<div>
<strong>{event.title}</strong>
{event.description && (
<>
{': '}
{event.description}
</>
)}
{statusPageUrl && (
<>
{' '}
<a
href={statusPageUrl}
target="_blank"
rel="noopener noreferrer"
>
View status page
</a>
</>
)}
</div>
),
backgroundColor: getEventSeverityColor(event.icon),
};
});
} catch (error) {
console.error('Error fetching UptimeRobot messages:', error);
return [];
}
}
}
16 changes: 16 additions & 0 deletions src/shared/lib/statusProviders/index.ts
Original file line number Diff line number Diff line change
@@ -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
];
115 changes: 115 additions & 0 deletions src/shared/lib/statusProviders/uptimeRobot.ts
Original file line number Diff line number Diff line change
@@ -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<UptimeRobotEventFeedResponse | null> {
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
}
}