diff --git a/frontend/dashboard/src/main.tsx b/frontend/dashboard/src/main.tsx
index c270a4a27..5b9b63b57 100644
--- a/frontend/dashboard/src/main.tsx
+++ b/frontend/dashboard/src/main.tsx
@@ -10,6 +10,7 @@ import SingleTemplatePage from "./routes/SingleTemplatePage";
import SingleWorkflowPage from "./routes/SingleWorkflowPage";
import ErrorPage from "./ErrorPage";
import { ThemeProvider, DiamondTheme } from "@diamondlightsource/sci-react-ui";
+import TriggersListPage from "./routes/TriggersListPage";
const router = createBrowserRouter([
{
@@ -45,6 +46,10 @@ const router = createBrowserRouter([
path: "workflows/:visitid/:workflowId/:tasknames",
element: ,
},
+ {
+ path: "triggers",
+ element: ,
+ },
]);
async function startMockingIfConfigured() {
diff --git a/frontend/dashboard/src/routes/TriggersListPage.tsx b/frontend/dashboard/src/routes/TriggersListPage.tsx
new file mode 100644
index 000000000..2d67f9f70
--- /dev/null
+++ b/frontend/dashboard/src/routes/TriggersListPage.tsx
@@ -0,0 +1,18 @@
+import { Breadcrumbs } from "@diamondlightsource/sci-react-ui";
+import { Box, Container } from "@mui/material";
+import { Link } from "react-router-dom";
+import { TriggersListView, WorkflowsNavbar } from "relay-workflows-lib";
+
+export default function TriggersListPage() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/relay-workflows-lib/lib/components/TriggersListContent.tsx b/frontend/relay-workflows-lib/lib/components/TriggersListContent.tsx
new file mode 100644
index 000000000..fe884d357
--- /dev/null
+++ b/frontend/relay-workflows-lib/lib/components/TriggersListContent.tsx
@@ -0,0 +1,78 @@
+import { useEffect } from "react";
+import { PreloadedQuery, usePreloadedQuery } from "react-relay";
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Typography,
+} from "@mui/material";
+import { PaginationControls } from "workflows-lib";
+import { TriggersListViewQuery } from "../views/TriggersListView";
+import type { TriggersListViewQuery as TriggersListViewQueryType } from "../views/__generated__/TriggersListViewQuery.graphql";
+
+interface TriggersListContentProps {
+ queryRef: PreloadedQuery;
+ currentPage: number;
+ totalPages: number;
+ selectedLimit: number;
+ onPageChange: (
+ page: number,
+ endCursor?: string | null,
+ hasNextPage?: boolean,
+ ) => void;
+ onLimitChange: (limit: number) => void;
+ updatePageInfo: (hasNextPage: boolean, endCursor: string | null) => void;
+}
+
+export default function TriggersListContent({
+ queryRef,
+ currentPage,
+ totalPages,
+ selectedLimit,
+ onPageChange,
+ onLimitChange,
+ updatePageInfo,
+}: TriggersListContentProps) {
+ const queryData = usePreloadedQuery(TriggersListViewQuery, queryRef);
+ const { nodes, pageInfo } = queryData.triggers;
+
+ useEffect(() => {
+ updatePageInfo(pageInfo.hasNextPage, pageInfo.endCursor ?? null);
+ }, [pageInfo.hasNextPage, pageInfo.endCursor, updatePageInfo]);
+
+ return (
+
+
+ {nodes.map((node, index) => (
+
+
+
+
+ {node.beamline}
+
+ {node.name}
+
+
+
+
+ Created from the {node.templateRef} template
+
+
+
+ ))}
+
+ {
+ onPageChange(page, pageInfo.endCursor, pageInfo.hasNextPage);
+ }}
+ selectedLimit={selectedLimit}
+ onLimitChange={onLimitChange}
+ />
+
+ );
+}
diff --git a/frontend/relay-workflows-lib/lib/components/WorkflowsNavbar.tsx b/frontend/relay-workflows-lib/lib/components/WorkflowsNavbar.tsx
index 6e7b19a8b..69382915f 100644
--- a/frontend/relay-workflows-lib/lib/components/WorkflowsNavbar.tsx
+++ b/frontend/relay-workflows-lib/lib/components/WorkflowsNavbar.tsx
@@ -1,8 +1,7 @@
-import { Box, Typography } from "@mui/material";
+import { Box } from "@mui/material";
import { NavLink as Link } from "react-router-dom";
import {
Navbar,
- DiamondTheme,
NavLinks,
NavLink,
User,
@@ -32,7 +31,7 @@ const handleLogout = () => {
}
};
-const WorkflowsNavbar: React.FC = ({ sessionInfo }) => {
+const WorkflowsNavbar: React.FC = () => {
const [user, setUser] = useState(null);
useEffect(() => {
@@ -67,31 +66,14 @@ const WorkflowsNavbar: React.FC = ({ sessionInfo }) => {
Templates
+
+ Triggers
+
}
rightSlot={
- <>
- {sessionInfo && (
-
- {sessionInfo}
-
- )}
-
- >
+
}
/>
);
diff --git a/frontend/relay-workflows-lib/lib/main.ts b/frontend/relay-workflows-lib/lib/main.ts
index 9f839ecfd..c06c3279d 100644
--- a/frontend/relay-workflows-lib/lib/main.ts
+++ b/frontend/relay-workflows-lib/lib/main.ts
@@ -11,6 +11,7 @@ export {
export { default as TemplateView } from "./views/TemplateView";
export { default as TemplateViewRetrigger } from "./views/TemplateViewRetrigger";
export { default as SingleWorkflowView } from "./views/SingleWorkflowView";
+export { default as TriggersListView } from "./views/TriggersListView";
export { default as RepositoryLink } from "./query-components/RepositoryLink";
export { default as WorkflowsNavbar } from "./components/WorkflowsNavbar";
export {
diff --git a/frontend/relay-workflows-lib/lib/views/TriggersListView.tsx b/frontend/relay-workflows-lib/lib/views/TriggersListView.tsx
new file mode 100644
index 000000000..a53d410df
--- /dev/null
+++ b/frontend/relay-workflows-lib/lib/views/TriggersListView.tsx
@@ -0,0 +1,70 @@
+import { graphql } from "relay-runtime";
+import { type TriggersListViewQuery as TriggersListViewQueryType } from "./__generated__/TriggersListViewQuery.graphql";
+import { useQueryLoader } from "react-relay";
+import { useCallback, useEffect } from "react";
+import TriggersListContent from "../components/TriggersListContent";
+import { Box } from "@mui/material";
+import { useServerSidePagination } from "../utils/useServerSidePagination";
+
+export const TriggersListViewQuery = graphql`
+ query TriggersListViewQuery($limit: Int, $cursor: String) {
+ triggers(limit: $limit, cursor: $cursor) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ nodes {
+ name
+ beamline
+ templateRef
+ }
+ }
+ }
+`;
+
+export default function TriggersListView() {
+ const [queryReference, loadQuery] = useQueryLoader(
+ TriggersListViewQuery,
+ );
+
+ const {
+ cursor,
+ currentPage,
+ totalPages,
+ selectedLimit,
+ goToPage,
+ changeLimit,
+ updatePageInfo,
+ } = useServerSidePagination();
+
+ const load = useCallback(() => {
+ loadQuery(
+ { limit: selectedLimit, cursor },
+ { fetchPolicy: "store-and-network" },
+ );
+ }, [selectedLimit, cursor, loadQuery]);
+
+ useEffect(() => {
+ load();
+ const interval = setInterval(load, 5000);
+ return () => {
+ clearInterval(interval);
+ };
+ }, [load]);
+
+ return (
+
+ {queryReference && (
+
+ )}
+
+ );
+}
diff --git a/frontend/relay-workflows-lib/tests/components/WorkflowsNavbar.test.tsx b/frontend/relay-workflows-lib/tests/components/WorkflowsNavbar.test.tsx
index 63f535c5c..5ac0896fc 100644
--- a/frontend/relay-workflows-lib/tests/components/WorkflowsNavbar.test.tsx
+++ b/frontend/relay-workflows-lib/tests/components/WorkflowsNavbar.test.tsx
@@ -26,31 +26,6 @@ describe("WorkflowsNavbar", () => {
vi.restoreAllMocks();
});
- it("renders with title and sessionInfo", () => {
- const { getByText } = render(
-
-
-
-
- ,
- );
- expect(getByText("cm12345-6")).toBeInTheDocument();
- });
-
- it("applies the correct styles", () => {
- const { getByText } = render(
-
-
-
-
- ,
- );
- const sessionElement = getByText("cm34567-8");
- expect(sessionElement).toHaveStyle(
- `color: ${DiamondTheme.palette.primary.contrastText}`,
- );
- });
-
it("displays the logged in user", async () => {
vi.mocked(getUser).mockReturnValue(Promise.resolve(testUser));
render(
diff --git a/frontend/workflows-lib/lib/components/triggers/TriggerList.tsx b/frontend/workflows-lib/lib/components/triggers/TriggerList.tsx
new file mode 100644
index 000000000..e69de29bb