Skip to content

Commit 2d9f1a2

Browse files
authored
New tab where users can browse matching and reaction rules (#22)
1 parent 573d222 commit 2d9f1a2

9 files changed

Lines changed: 627 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from "react";
2+
import Tooltip from "@mui/material/Tooltip";
3+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
4+
import { useNotifications } from "./NotificationProvider";
5+
import { MinimalIconButton } from "./MinimalIconButton";
6+
7+
// Copies `text` to the clipboard on click, with a toast for success/failure --
8+
// same pattern UserIconDropdown's session-id copy uses, pulled out for reuse
9+
// wherever a raw string (a SMILES, a reaction SMARTS, ...) needs a copy affordance.
10+
export function CopyIconButton({ text, label = "value" }: { text: string; label?: string }) {
11+
const { pushNotification } = useNotifications();
12+
13+
const handleCopy = async (event: React.MouseEvent) => {
14+
event.stopPropagation();
15+
try {
16+
await navigator.clipboard.writeText(text);
17+
pushNotification(`Copied ${label} to clipboard`, "success");
18+
} catch (err) {
19+
pushNotification(`Failed to copy ${label}`, "error");
20+
}
21+
};
22+
23+
return (
24+
<Tooltip title={`Copy ${label}`} arrow>
25+
<MinimalIconButton onClick={handleCopy}>
26+
<ContentCopyIcon fontSize="inherit" />
27+
</MinimalIconButton>
28+
</Tooltip>
29+
);
30+
}

gui/src/client/src/components/MenuContent.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ExploreIcon from "@mui/icons-material/Explore";
99
import BarChartIcon from "@mui/icons-material/BarChart";
1010
import HomeRoundedIcon from "@mui/icons-material/HomeRounded";
1111
import UploadFileIcon from "@mui/icons-material/UploadFile";
12+
import RuleIcon from "@mui/icons-material/Rule";
1213
import { useNavigate, useLocation } from "react-router-dom";
1314

1415
const mainListItems = [
@@ -32,6 +33,11 @@ const mainListItems = [
3233
icon: <BarChartIcon />,
3334
to: `/dashboard/enrichment`
3435
},
36+
{
37+
text: "Rules",
38+
icon: <RuleIcon />,
39+
to: `/dashboard/rules`
40+
},
3541
]
3642

3743
interface MenuItemProps {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Renders a reaction rule's scheme -- reactant(s) -> product(s) -- as an SVG
2+
// rendered server-side by RDKit (see fetchReactionSchemeSvg / routes/rules.py's
3+
// reaction_scheme_svg). This replaced an earlier client-side attempt built on
4+
// smiles-drawer's ReactionDrawer/parseReaction: that API parses with the same
5+
// strict, SMILES-only grammar `SmilesDrawer.parse` uses, so a single SMARTS-only
6+
// token anywhere in the reaction (an OR-list like "[C,c]", a boolean primitive like
7+
// "[O&D2]"/"[C;!R]") aborted the whole parse -- most of this ruleset's reaction
8+
// rules use at least one such token. RDKit's own reaction drawer understands full
9+
// SMARTS query syntax directly, so every rule renders, not just the plain-enough
10+
// handful a SMILES grammar could parse.
11+
import React from "react";
12+
import Alert from "@mui/material/Alert";
13+
import Box from "@mui/material/Box";
14+
import CircularProgress from "@mui/material/CircularProgress";
15+
import DOMPurify from "dompurify";
16+
import { useColorScheme } from "@mui/material/styles";
17+
import { useQuery } from "@tanstack/react-query";
18+
import { fetchReactionSchemeSvg } from "../../features/rules/api";
19+
20+
export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) {
21+
const { mode, systemMode } = useColorScheme();
22+
const theme = (systemMode !== undefined ? systemMode : mode) === "dark" ? "dark" : "light";
23+
24+
const svgQuery = useQuery({
25+
queryKey: ["reactionSchemeSvg", id, theme],
26+
queryFn: ({ signal }) => fetchReactionSchemeSvg(id, theme, signal),
27+
staleTime: Infinity,
28+
gcTime: Infinity,
29+
});
30+
31+
// Server-rendered SVG markup gets injected raw via dangerouslySetInnerHTML, so it
32+
// must be sanitized first -- same rationale/profile as SvgViewer.
33+
const sanitizedSvg = React.useMemo(
34+
() => (svgQuery.data ? DOMPurify.sanitize(svgQuery.data, { USE_PROFILES: { svg: true, svgFilters: true } }) : null),
35+
[svgQuery.data]
36+
);
37+
38+
if (svgQuery.isLoading) {
39+
return (
40+
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", height: 100 }}>
41+
<CircularProgress size={20} />
42+
</Box>
43+
);
44+
}
45+
46+
if (svgQuery.error || !sanitizedSvg) {
47+
return (
48+
<Alert severity="warning" variant="outlined" sx={{ py: 0 }}>
49+
Could not render this reaction ({smarts}).
50+
</Alert>
51+
);
52+
}
53+
54+
return (
55+
<Box
56+
sx={{ "& svg": { display: "block", maxWidth: "100%", height: "auto" } }}
57+
dangerouslySetInnerHTML={{ __html: sanitizedSvg }}
58+
/>
59+
);
60+
}

gui/src/client/src/components/workspace/Workspace.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { WorkspaceHeader } from "./WorkspaceHeader";
1414
import { WorkspaceHome } from "./WorkspaceHome";
1515
import { WorkspaceUpload } from "./WorkspaceUpload";
1616
import { WorkspaceDiscovery } from "./WorkspaceDiscovery";
17+
import { WorkspaceRules } from "./WorkspaceRules";
1718
// import { WorkspaceEnrichment } from "./tabs/enrichment/WorkspaceEnrichment";
1819

1920
export const Workspace: React.FC = () => {
@@ -172,6 +173,7 @@ export const Workspace: React.FC = () => {
172173
<Route path="discovery" element={<WorkspaceDiscovery session={session} setSession={setSession} />} />
173174
{/*<Route path="enrichment" element={<WorkspaceEnrichment session={session} setSession={setSession} />} />*/}
174175
<Route path="enrichment" element={<div>Analysis currently available. Check back later.</div>} />
176+
<Route path="rules" element={<WorkspaceRules />} />
175177
</Routes>
176178
</Box>
177179
</Fade>

0 commit comments

Comments
 (0)