diff --git a/public/api/resource-groups/1/announcements.json b/public/api/resource-groups/1/announcements.json
new file mode 100644
index 0000000..a3d63d4
--- /dev/null
+++ b/public/api/resource-groups/1/announcements.json
@@ -0,0 +1,9 @@
+{
+ "announcements": [
+ {
+ "type": "outage",
+ "description": "Bridges-2 compute, storage, and virtual machines will be unavailable on September 1, 2024 from 8:00 a.m. to 5:00 p.m. EDT for scheduled filesystem maintenance.",
+ "announcementUri": "https://www.psc.edu/resources/bridges-2/"
+ }
+ ]
+}
diff --git a/public/api/resource-groups/1/events.json b/public/api/resource-groups/1/events.json
new file mode 100644
index 0000000..3512c67
--- /dev/null
+++ b/public/api/resource-groups/1/events.json
@@ -0,0 +1,12 @@
+{
+ "events": [
+ {
+ "title": "Bridges-2 Webinar: Scaling Up Ecological Monitoring with AI: How Supercomputing Is Unlocking the Value of Autonomous Acoustic Sensing",
+ "startDateTime": "2024-08-23T13:00:00-04:00",
+ "endDateTime": "2024-08-23T14:00:00-04:00",
+ "eventUri": "https://www.psc.edu/events/bridges-2-webinar-series/scaling-up-ecological-monitoring-with-ai/",
+ "speaker": "Sam Lapp, University of Pittsburgh",
+ "description": "Automated acoustic recording devices monitor wildlife at an unprecedented scale. Using hundreds of sensors, bioacoustic monitoring transforms our understanding of birds, bats, and frogs, aiding biodiversity management."
+ }
+ ]
+}
diff --git a/src/alert.css b/src/alert.css
new file mode 100644
index 0000000..1e3e47c
--- /dev/null
+++ b/src/alert.css
@@ -0,0 +1,12 @@
+.alert {
+ align-items: center;
+ background-color: var(--yellow-200);
+ border-radius: 10px;
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 20px;
+ padding: 20px;
+}
+.alert i {
+ margin-right: 20px;
+}
diff --git a/src/alert.jsx b/src/alert.jsx
new file mode 100644
index 0000000..2c46bcb
--- /dev/null
+++ b/src/alert.jsx
@@ -0,0 +1,10 @@
+import Icon from "./icon";
+
+export default function Alert({ children, icon = "megaphone-fill" }) {
+ return (
+
+ {icon ?
: null}
+
{children}
+
+ );
+}
diff --git a/src/resource-catalog.jsx b/src/resource-catalog.jsx
index 3bf364e..75a3d67 100644
--- a/src/resource-catalog.jsx
+++ b/src/resource-catalog.jsx
@@ -1,5 +1,6 @@
import { ErrorBoundary, lazy, LocationProvider, Router } from "preact-iso";
import accordionStyle from "./accordion.css?inline";
+import alertStyle from "./alert.css?inline";
import baseStyle from "./base.css?inline";
import componentsStyle from "./components.css?inline";
import contentStyle from "./content.css?inline";
@@ -11,6 +12,7 @@ import gridStyle from "./grid.css?inline";
import carouselStyle from "./carousel.css?inline";
import resourceFiltersStyle from "./resource-filters.css?inline";
import resourceGroupStyle from "./resource-group.css?inline";
+import resourceGroupEventStyle from "./resource-group-event.css?inline";
import searchStyle from "./search.css?inline";
import tagsStyle from "./tags.css?inline";
import tippyStyle from "tippy.js/dist/tippy.css?inline";
@@ -49,6 +51,7 @@ export function ResourceCatalog({
+
@@ -57,6 +60,7 @@ export function ResourceCatalog({
+
diff --git a/src/resource-group-detail.jsx b/src/resource-group-detail.jsx
index 861054a..49eef6b 100644
--- a/src/resource-group-detail.jsx
+++ b/src/resource-group-detail.jsx
@@ -1,4 +1,5 @@
import ResourceGroupDescription from "./resource-group-description";
+import ResourceGroupEvents from "./resource-group-events";
import ResourceGroupQueueMetrics from "./resource-group-queue-metrics";
import ResourceGroupResources from "./resource-group-resources";
import ResourceGroupSoftware from "./resource-group-software";
@@ -14,6 +15,10 @@ export default function ResourceGroupDetail({ baseUri, resourceGroupId }) {
baseUri={baseUri}
resourceGroupId={resourceGroupId}
/>
+
{title} : title;
+ const metadata = [];
+ let icon = null;
+
+ if (startDateTime) {
+ const start = new Date(startDateTime);
+ const end = new Date(endDateTime || startDateTime);
+ const [startDate, endDate] = [start, end].map((date) =>
+ date.toLocaleString("en-US", { dateStyle: "long" })
+ );
+ const [startTime, endTime] = [start, end].map((date) =>
+ date.toLocaleString("en-US", { timeStyle: "short" })
+ );
+ const tz = start
+ .toLocaleTimeString("en-US", { timeZoneName: "short" })
+ .split(" ")[2];
+ const parts = [`${startDate},`, startTime];
+ if (endTime != startTime || endDate != startDate) {
+ parts.push("-");
+ if (endDate != startDate) parts.push(`${endDate},`);
+ parts.push(endTime);
+ }
+ parts.push(`(${tz})`);
+ metadata.push(, parts.join(" "));
+
+ const iconContent = start
+ .toLocaleString("en-US", { dateStyle: "medium" })
+ .split(",")[0]
+ .split(" ")
+ .map((part) => {part});
+ icon = eventUri ? (
+
+ {iconContent}
+
+ ) : (
+ {iconContent}
+ );
+ }
+
+ if (speaker) metadata.push(, speaker);
+
+ return (
+
+ {icon}
+
+
{headingContent}
+ {metadata.length ?
{metadata}
: null}
+ {description ?
{description}
: null}
+
+
+ );
+}
diff --git a/src/resource-group-events.jsx b/src/resource-group-events.jsx
new file mode 100644
index 0000000..76b16dd
--- /dev/null
+++ b/src/resource-group-events.jsx
@@ -0,0 +1,37 @@
+import { useJSON } from "./utils";
+
+import Alert from "./alert";
+import Icon from "./icon";
+import ResourceGroupEvent from "./resource-group-event";
+
+export default function ResourceGroupEvents({ baseUri, resourceGroupId }) {
+ const announcementData = useJSON(
+ `${baseUri}/api/resource-groups/${resourceGroupId}/announcements.json`,
+ null
+ );
+ const eventData = useJSON(
+ `${baseUri}/api/resource-groups/${resourceGroupId}/events.json`,
+ null
+ );
+ if (!announcementData && !eventData) return;
+ return (
+ <>
+
+
+ Announcements and Events
+
+ {announcementData &&
+ announcementData.announcements.map(
+ ({ description, announcementUri }) => (
+
+ {description}{" "}
+ {announcementUri ? (
+ Learn more.
+ ) : null}
+
+ )
+ )}
+ {eventData && eventData.events.map((event) => ResourceGroupEvent(event))}
+ >
+ );
+}