(0);
+ const [isLoadingScripts, setIsLoadingScripts] = useState(false);
// Fetch user roles
useEffect(() => {
@@ -361,6 +365,45 @@ export default function ReviewerDashboardPage() {
return () => controller.abort();
}, [userRoles]);
+ // Fetch script policies needing review
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function fetchScriptPolicies() {
+ if (!userRoles || !hasRole(userRoles, RolesConstants.ScriptWrite)) return;
+
+ setIsLoadingScripts(true);
+ try {
+ const res = await fetch(
+ '/v1/script-policy?Page=1&Limit=50&Policy=0',
+ { signal: controller.signal }
+ );
+
+ if (!res.ok) {
+ console.error('Failed to fetch script policies:', res.status);
+ setIsLoadingScripts(false);
+ return;
+ }
+
+ const policies = await res.json();
+ // The API does not provide total count, so we use the length of returned policies
+ setScriptPoliciesCount(Array.isArray(policies) ? policies.length : 0);
+ } catch (error) {
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
+ console.error("Error fetching script policies:", error);
+ }
+ } finally {
+ setIsLoadingScripts(false);
+ }
+ }
+
+ if (userRoles) {
+ fetchScriptPolicies();
+ }
+
+ return () => controller.abort();
+ }, [userRoles]);
+
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
@@ -414,6 +457,7 @@ export default function ReviewerDashboardPage() {
hasRole(userRoles, RolesConstants.MapfixUpload) ||
hasRole(userRoles, RolesConstants.ScriptWrite)
);
+ const canReviewScripts = hasRole(userRoles, RolesConstants.ScriptWrite);
if (!hasAnyReviewerRole(userRoles)) {
return (
@@ -470,7 +514,7 @@ export default function ReviewerDashboardPage() {
{/* Summary Cards */}
@@ -521,6 +565,43 @@ export default function ReviewerDashboardPage() {
)}
+
+ {canReviewScripts && (
+
+
+
+
+
+
+
+ {isLoadingScripts ? (
+
+ ) : (
+ scriptPoliciesCount >= 50 ? '50+' : scriptPoliciesCount
+ )}
+
+
+ Scripts Pending Review
+
+
+
+
+
+
+
+ )}
{/* Tabs */}
diff --git a/web/src/app/script-review/page.tsx b/web/src/app/script-review/page.tsx
new file mode 100644
index 0000000..914b685
--- /dev/null
+++ b/web/src/app/script-review/page.tsx
@@ -0,0 +1,1165 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import Editor, { DiffEditor } from "@monaco-editor/react";
+
+// MUI Components
+import {
+ Typography,
+ Box,
+ Snackbar,
+ Alert,
+ LinearProgress,
+ Container,
+ CircularProgress,
+} from "@mui/material";
+
+import Webpage from "@/app/_components/webpage";
+
+// MUI Icons
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import BlockIcon from '@mui/icons-material/Block';
+import DeleteIcon from '@mui/icons-material/Delete';
+import FindReplaceIcon from '@mui/icons-material/FindReplace';
+import CodeIcon from '@mui/icons-material/Code';
+import WarningAmberIcon from '@mui/icons-material/WarningAmber';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
+import HomeIcon from '@mui/icons-material/Home';
+import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
+import SendIcon from '@mui/icons-material/Send';
+
+import { ErrorDisplay } from "@/app/_components/ErrorDisplay";
+import { ScriptPolicy, PolicyType, PolicyLabels } from "@/app/ts/ScriptPolicy";
+import { Script } from "@/app/ts/Script";
+import { useTitle } from "@/app/hooks/useTitle";
+import { useUser } from "@/app/hooks/useUser";
+import { RolesConstants, hasRole } from "@/app/ts/Roles";
+
+interface SnackbarState {
+ open: boolean;
+ message: string | null;
+ severity: 'success' | 'error' | 'info' | 'warning';
+}
+
+// IDE Button Component
+const IDEButton = ({
+ children,
+ onClick,
+ disabled,
+ variant = 'default',
+ fullWidth = false,
+ icon,
+}: {
+ children: React.ReactNode;
+ onClick?: () => void;
+ disabled?: boolean;
+ variant?: 'default' | 'primary' | 'success' | 'error' | 'warning';
+ fullWidth?: boolean;
+ icon?: React.ReactNode;
+}) => {
+ const getColors = () => {
+ switch (variant) {
+ case 'primary':
+ return {
+ bg: '#0e639c',
+ hoverBg: '#1177bb',
+ activeBg: '#007acc',
+ color: '#ffffff',
+ border: '#007acc',
+ };
+ case 'success':
+ return {
+ bg: '#0e7e0e',
+ hoverBg: '#0f9d0f',
+ activeBg: '#14b814',
+ color: '#ffffff',
+ border: '#14b814',
+ };
+ case 'error':
+ return {
+ bg: '#7e0e0e',
+ hoverBg: '#9d0f0f',
+ activeBg: '#b81414',
+ color: '#ffffff',
+ border: '#b81414',
+ };
+ case 'warning':
+ return {
+ bg: '#7e5e0e',
+ hoverBg: '#9d750f',
+ activeBg: '#b88614',
+ color: '#ffffff',
+ border: '#b88614',
+ };
+ default:
+ return {
+ bg: 'transparent',
+ hoverBg: 'rgba(255, 255, 255, 0.08)',
+ activeBg: 'rgba(255, 255, 255, 0.12)',
+ color: '#cccccc',
+ border: '#3e3e42',
+ };
+ }
+ };
+
+ const colors = getColors();
+
+ return (
+
+ );
+};
+
+// IDE Info Badge Component
+const InfoBadge = ({
+ children,
+ type = 'info',
+ icon,
+}: {
+ children: React.ReactNode;
+ type?: 'info' | 'warning' | 'error' | 'success';
+ icon?: React.ReactNode;
+}) => {
+ const getColors = () => {
+ switch (type) {
+ case 'warning':
+ return { bg: 'rgba(250, 200, 90, 0.15)', border: '#fac85a', color: '#fac85a' };
+ case 'error':
+ return { bg: 'rgba(240, 82, 82, 0.15)', border: '#f05252', color: '#f05252' };
+ case 'success':
+ return { bg: 'rgba(80, 200, 120, 0.15)', border: '#50c878', color: '#50c878' };
+ default:
+ return { bg: 'rgba(100, 150, 230, 0.15)', border: '#6496e6', color: '#6496e6' };
+ }
+ };
+
+ const colors = getColors();
+
+ return (
+
+ {icon && {icon}}
+ {children}
+
+ );
+};
+
+export default function ScriptReviewPage() {
+ const navigate = useNavigate();
+ const { user, isLoading: userLoading } = useUser();
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [scriptPolicy, setScriptPolicy] = useState(null);
+ const [script, setScript] = useState