diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml index f19a6d403a92..52758378212b 100644 --- a/.github/workflows/cipp_dev_build.yml +++ b/.github/workflows/cipp_dev_build.yml @@ -38,6 +38,13 @@ jobs: - name: Build Project run: npm run build + # Update version.json with commit hash + - name: Update version.json + run: | + VERSION=$(jq -r '.version' public/version.json) + SHORT_SHA="${GITHUB_SHA::7}" + echo "{\"version\": \"${VERSION}\", \"commit\": \"${SHORT_SHA}\"}" > out/version.json + # Create ZIP File in a New Source Directory - name: Prepare and Zip Build Files run: | diff --git a/package.json b/package.json index 05a3468c52e4..0b146367a28c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.2.1", + "version": "10.2.2", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 6fafb4fbb418..2550102c4f0b 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.2.1" + "version": "10.2.2" } \ No newline at end of file diff --git a/src/components/CippComponents/CippSponsor.jsx b/src/components/CippComponents/CippSponsor.jsx index ae4021917def..1a39eb361013 100644 --- a/src/components/CippComponents/CippSponsor.jsx +++ b/src/components/CippComponents/CippSponsor.jsx @@ -72,7 +72,7 @@ export const CippSponsor = () => { This application is sponsored by @@ -82,6 +82,7 @@ export const CippSponsor = () => { justifyContent: "center", alignItems: "center", height: "55px", + mb: 1, }} > @@ -92,7 +93,7 @@ export const CippSponsor = () => { cursor: "pointer", maxHeight: "50px", width: "auto", - maxWidth: "100px", + maxWidth: "150px", }} onClick={() => window.open(randomimg.link)} /> diff --git a/src/components/CippSettings/CippSiemSettings.jsx b/src/components/CippSettings/CippSiemSettings.jsx new file mode 100644 index 000000000000..bda03174c426 --- /dev/null +++ b/src/components/CippSettings/CippSiemSettings.jsx @@ -0,0 +1,127 @@ +import { Button, Typography, Alert, Box, TextField, InputAdornment } from "@mui/material"; +import { Key } from "@mui/icons-material"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { useForm } from "react-hook-form"; + +const CippSiemSettings = () => { + const generateSas = ApiPostCall({ + datafromUrl: true, + }); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + Days: { label: "365 days", value: "365" }, + }, + }); + + const handleGenerate = () => { + const formData = formControl.getValues(); + const days = formData.Days?.value ?? "365"; + generateSas.mutate({ + url: "/api/ExecCippLogsSas", + data: { Days: parseInt(days, 10) }, + queryKey: "ExecCippLogsSas", + }); + }; + + return ( + } + > + Generate SAS Token + + } + > + + + Generate a read-only SAS token for the CIPP Logs table. This token can be used to query + log data from external SIEM tools or scripts using the Azure Table Storage REST API. Note + that generating a new URL does not invalidate previous URLs. + + + + + + + {generateSas.isError && ( + + {generateSas.error?.response?.data?.Results || + generateSas.error?.message || + "Failed to generate SAS token"} + + )} + + {generateSas.isSuccess && generateSas.data?.data?.Results && ( + <> + + SAS URL generated successfully. Copy this for your records, it will only be shown + once. + + + + SAS URL + + + + + ), + }, + }} + /> + + + + Expires On + + + {new Date(generateSas.data.data.Results.ExpiresOn).toLocaleString()} + + + + )} + + + ); +}; + +export default CippSiemSettings; diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index dd59cee0bccc..1a2811d462c3 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -257,7 +257,7 @@ const CippStandardAccordion = ({ initialConfigured[standardName] = isStandardConfigured( standardName, standard, - currentValues + currentValues, ); } }); @@ -271,6 +271,48 @@ const CippStandardAccordion = ({ } }, [watchedValues, selectedStandards, editMode]); + // Sync internal state when selectedStandards keys change (e.g., after re-indexing on removal) + useEffect(() => { + const currentKeys = Object.keys(selectedStandards); + const stateKeys = Object.keys(savedValues); + if (stateKeys.length === 0) return; + + const currentSet = new Set(currentKeys); + const stateSet = new Set(stateKeys); + + const removedKeys = stateKeys.filter((k) => !currentSet.has(k)); + const addedKeys = currentKeys.filter((k) => !stateSet.has(k)); + + if (removedKeys.length > 0 || addedKeys.length > 0) { + setSavedValues((prev) => { + const updated = { ...prev }; + removedKeys.forEach((k) => delete updated[k]); + addedKeys.forEach((k) => { + const currentValues = _.get(watchedValues, k); + if (currentValues) { + updated[k] = _.cloneDeep(currentValues); + } + }); + return updated; + }); + + setConfiguredState((prev) => { + const updated = { ...prev }; + removedKeys.forEach((k) => delete updated[k]); + addedKeys.forEach((k) => { + const baseStandardName = k.split("[")[0]; + const standard = providedStandards.find((s) => s.name === baseStandardName); + const currentValues = _.get(watchedValues, k); + if (standard && currentValues) { + updated[k] = isStandardConfigured(k, standard, currentValues); + } + }); + return updated; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedStandards]); + // Save changes for a standard const handleSave = (standardName, standard, current) => { // Clone the current values to avoid reference issues @@ -587,8 +629,8 @@ const CippStandardAccordion = ({ const accordionTitle = templateDisplayName ? `${standard.label} - ${templateDisplayName}` : selectedTemplateName && _.get(selectedTemplateName, "label") - ? `${standard.label} - ${_.get(selectedTemplateName, "label")}` - : standard.label; + ? `${standard.label} - ${_.get(selectedTemplateName, "label")}` + : standard.label; // Get current values and check if they differ from saved values const current = _.get(watchedValues, standardName); @@ -598,7 +640,7 @@ const CippStandardAccordion = ({ // Check if all required fields are filled const requiredFieldsFilled = current - ? standard.addedComponent?.every((component) => { + ? (standard.addedComponent?.every((component) => { // Always skip switches regardless of their required property if (component.type === "switch") return true; @@ -630,7 +672,7 @@ const CippStandardAccordion = ({ switch (compareType) { case "valueEq": conditionMet = conditionValue.some( - (item) => item?.[propertyName] === compareValue + (item) => item?.[propertyName] === compareValue, ); break; default: @@ -658,7 +700,7 @@ const CippStandardAccordion = ({ // For other field types return !!fieldValue; - }) ?? true + }) ?? true) : false; // ALWAYS require an action for all standards @@ -668,7 +710,7 @@ const CippStandardAccordion = ({ const hasRequiredComponents = standard.addedComponent && standard.addedComponent.some( - (comp) => comp.type !== "switch" && comp.required !== false + (comp) => comp.type !== "switch" && comp.required !== false, ); // Action is always required and must be an array with at least one element @@ -904,7 +946,7 @@ const CippStandardAccordion = ({ component={component} formControl={formControl} /> - ) + ), )} )} @@ -962,7 +1004,7 @@ const CippStandardAccordion = ({ component={component} formControl={formControl} /> - ) + ), )} diff --git a/src/contexts/settings-context.js b/src/contexts/settings-context.js index bbc7f8a60de5..f6f61ffc8649 100644 --- a/src/contexts/settings-context.js +++ b/src/contexts/settings-context.js @@ -124,6 +124,12 @@ export const SettingsProvider = (props) => { } }, []); + useEffect(() => { + if (state.isInitialized) { + storeSettings(state); + } + }, [state]); + const handleReset = useCallback(() => { deleteSettings(); setState((prevState) => ({ @@ -142,11 +148,6 @@ export const SettingsProvider = (props) => { return acc; }, {}); - storeSettings({ - ...prevState, - ...filteredSettings, - }); - return { ...prevState, ...filteredSettings, @@ -171,17 +172,13 @@ export const SettingsProvider = (props) => { handleUpdate, isCustom, setLastUsedFilter: (page, filter) => { - setState((prevState) => { - const updated = { - ...prevState, - lastUsedFilters: { - ...prevState.lastUsedFilters, - [page]: filter, - }, - }; - storeSettings(updated); - return updated; - }); + setState((prevState) => ({ + ...prevState, + lastUsedFilters: { + ...prevState.lastUsedFilters, + [page]: filter, + }, + })); }, }} > diff --git a/src/layouts/index.js b/src/layouts/index.js index 7a92342bd65d..b3c6ea7ba694 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -236,6 +236,7 @@ export const Layout = (props) => { var bookmarkLocked = settings.bookmarkLocked; var bookmarkSortOrder = settings.bookmarkSortOrder; var bookmarksOpen = settings.bookmarksOpen; + var compactNav = settings.compactNav; settings.handleUpdate({ ...userSettingsAPI.data, @@ -246,6 +247,7 @@ export const Layout = (props) => { bookmarkLocked, bookmarkSortOrder, bookmarksOpen, + compactNav, showDevtools, }); diff --git a/src/layouts/side-nav-bookmarks.js b/src/layouts/side-nav-bookmarks.js index 527b475efebb..229a86c88bbf 100644 --- a/src/layouts/side-nav-bookmarks.js +++ b/src/layouts/side-nav-bookmarks.js @@ -1,14 +1,6 @@ import { useCallback, useMemo, useState, useEffect, useRef } from "react"; import NextLink from "next/link"; -import { - Box, - ButtonBase, - Collapse, - IconButton, - Stack, - SvgIcon, - Typography, -} from "@mui/material"; +import { Box, ButtonBase, Collapse, IconButton, Stack, SvgIcon, Typography } from "@mui/material"; import BookmarkIcon from "@mui/icons-material/Bookmark"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; @@ -25,6 +17,8 @@ import { useSettings } from "../hooks/use-settings"; export const SideNavBookmarks = ({ collapse = false }) => { const settings = useSettings(); + const compactNav = settings.compactNav ?? false; + const navItemPy = compactNav ? "6px" : "12px"; const [open, setOpen] = useState(settings.bookmarksOpen ?? false); const reorderMode = settings.bookmarkReorderMode || "arrows"; const locked = settings.bookmarkLocked ?? false; @@ -54,7 +48,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { updatedBookmarks[index - 1] = temp; settings.handleUpdate({ bookmarks: updatedBookmarks }); }, - [settings] + [settings], ); const moveBookmarkDown = useCallback( @@ -67,7 +61,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { updatedBookmarks[index + 1] = temp; settings.handleUpdate({ bookmarks: updatedBookmarks }); }, - [settings] + [settings], ); const removeBookmark = useCallback( @@ -79,7 +73,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { settings.handleUpdate({ bookmarks: updatedBookmarks }); } }, - [settings] + [settings], ); const animatedMoveUp = useCallback( @@ -87,7 +81,10 @@ export const SideNavBookmarks = ({ collapse = false }) => { if (index <= 0 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index - 1]; - if (!el1 || !el2) { moveBookmarkUp(index); return; } + if (!el1 || !el2) { + moveBookmarkUp(index); + return; + } const distance = el1.getBoundingClientRect().top - el2.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index - 1, offset1: -distance, offset2: distance }); setTimeout(() => { @@ -95,7 +92,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { setAnimatingPair(null); }, 250); }, - [animatingPair, moveBookmarkUp] + [animatingPair, moveBookmarkUp], ); const animatedMoveDown = useCallback( @@ -104,7 +101,10 @@ export const SideNavBookmarks = ({ collapse = false }) => { if (index >= bookmarks.length - 1 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index + 1]; - if (!el1 || !el2) { moveBookmarkDown(index); return; } + if (!el1 || !el2) { + moveBookmarkDown(index); + return; + } const distance = el2.getBoundingClientRect().top - el1.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index + 1, offset1: distance, offset2: -distance }); setTimeout(() => { @@ -112,7 +112,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { setAnimatingPair(null); }, 250); }, - [animatingPair, settings.bookmarks, moveBookmarkDown] + [animatingPair, settings.bookmarks, moveBookmarkDown], ); const triggerSortFlash = useCallback(() => { @@ -149,11 +149,12 @@ export const SideNavBookmarks = ({ collapse = false }) => { const items = [...(settings.bookmarks || [])]; const [reordered] = items.splice(dragIndex, 1); items.splice(dropIndex, 0, reordered); - settings.handleUpdate({ bookmarks: items }); + settings.handleUpdate({ bookmarks: items, bookmarkSortOrder: "custom" }); + setSortOrder("custom"); setDragIndex(null); setDragOverIndex(null); }, - [dragIndex, settings] + [dragIndex, settings], ); const handleDragEnd = useCallback(() => { @@ -235,7 +236,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { size="small" onClick={handleToggleLock} sx={{ - color: locked ? "warning.main" : "neutral.500", + color: locked ? "neutral.500" : "warning.main", p: "2px", transition: "opacity 250ms ease-in-out", ...(flashLock && { @@ -271,7 +272,9 @@ export const SideNavBookmarks = ({ collapse = false }) => { }, }), }} - title={sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A"} + title={ + sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A" + } > {sortOrder === "custom" && } {sortOrder === "asc" && } @@ -300,7 +303,7 @@ export const SideNavBookmarks = ({ collapse = false }) => { - + {displayBookmarks.length === 0 ? (
  • { displayBookmarks.map((bookmark, idx) => (
  • { itemRefs.current[idx] = el; }} + ref={(el) => { + itemRefs.current[idx] = el; + }} data-bookmark-index={idx} - draggable={reorderMode === "drag" && sortOrder === "custom" && !locked} - {...(reorderMode === "drag" ? { - onDragStart: (e) => { - if (locked) { e.preventDefault(); triggerLockFlash(); return; } - if (sortOrder !== "custom") { e.preventDefault(); triggerSortFlash(); return; } - handleDragStart(idx); - }, - onDragEnd: handleDragEnd, - ...(sortOrder === "custom" && !locked ? { - onDragOver: (e) => handleDragOver(e, idx), - onDrop: (e) => handleDrop(e, idx), - } : {}), - } : {})} + draggable={reorderMode === "drag" && !locked} + {...(reorderMode === "drag" + ? { + onDragStart: (e) => { + if (locked) { + e.preventDefault(); + triggerLockFlash(); + return; + } + handleDragStart(idx); + }, + onDragEnd: handleDragEnd, + ...(!locked + ? { + onDragOver: (e) => handleDragOver(e, idx), + onDrop: (e) => handleDrop(e, idx), + } + : {}), + } + : {})} style={{ - ...(animatingPair && (animatingPair.idx1 === idx || animatingPair.idx2 === idx) ? { - transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, - transition: 'transform 250ms ease-in-out', - position: 'relative', - zIndex: animatingPair.idx1 === idx ? 1 : 0, - } : {}), + ...(animatingPair && (animatingPair.idx1 === idx || animatingPair.idx2 === idx) + ? { + transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, + transition: "transform 250ms ease-in-out", + position: "relative", + zIndex: animatingPair.idx1 === idx ? 1 : 0, + } + : {}), }} > { alignItems="center" sx={{ position: "relative", - pl: "42px", + pl: "6px", pr: "8px", - py: "2px", "&:hover .bookmark-controls": { opacity: 1, }, - ...(sortOrder === "custom" && reorderMode === "drag" && dragIndex === idx && { - opacity: 0.4, - }), - ...(sortOrder === "custom" && reorderMode === "drag" && dragOverIndex === idx && dragIndex !== idx && { - borderTop: "2px solid", - borderColor: "primary.main", - }), + ...(reorderMode === "drag" && + dragIndex === idx && { + opacity: 0.4, + }), + ...(reorderMode === "drag" && + dragOverIndex === idx && + dragIndex !== idx && { + borderTop: "2px solid", + borderColor: "primary.main", + }), }} > {reorderMode === "drag" && !locked && ( e.preventDefault()} onTouchStart={() => { - if (sortOrder !== "custom") { triggerSortFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } touchDragRef.current.startIdx = idx; setDragIndex(idx); }} @@ -381,7 +401,11 @@ export const SideNavBookmarks = ({ collapse = false }) => { const li = el?.closest("[data-bookmark-index]"); if (li) { const overIdx = parseInt(li.dataset.bookmarkIndex, 10); - if (!isNaN(overIdx) && overIdx >= 0 && overIdx < (settings.bookmarks || []).length) { + if ( + !isNaN(overIdx) && + overIdx >= 0 && + overIdx < (settings.bookmarks || []).length + ) { touchDragRef.current.overIdx = overIdx; setDragOverIndex(overIdx); } @@ -393,7 +417,8 @@ export const SideNavBookmarks = ({ collapse = false }) => { const items = [...(settings.bookmarks || [])]; const [reordered] = items.splice(startIdx, 1); items.splice(overIdx, 0, reordered); - settings.handleUpdate({ bookmarks: items }); + settings.handleUpdate({ bookmarks: items, bookmarkSortOrder: "custom" }); + setSortOrder("custom"); } touchDragRef.current = { startIdx: null, overIdx: null }; setDragIndex(null); @@ -401,11 +426,14 @@ export const SideNavBookmarks = ({ collapse = false }) => { }} sx={{ touchAction: "none", + position: "absolute", + left: "6px", + top: "50%", + transform: "translateY(-50%)", display: "flex", alignItems: "center", color: "neutral.500", - cursor: sortOrder === "custom" ? "grab" : "default", - mr: 0.5, + cursor: "grab", }} > @@ -422,7 +450,8 @@ export const SideNavBookmarks = ({ collapse = false }) => { fontSize: 13, fontWeight: 500, justifyContent: "flex-start", - py: "6px", + pl: "36px", + py: navItemPy, textAlign: "left", whiteSpace: "nowrap", flexGrow: 1, @@ -435,6 +464,9 @@ export const SideNavBookmarks = ({ collapse = false }) => { color: "text.secondary", overflow: "hidden", textOverflow: "ellipsis", + minHeight: "24px", + display: "flex", + alignItems: "center", }} > {bookmark.label} @@ -456,13 +488,16 @@ export const SideNavBookmarks = ({ collapse = false }) => { }, }} > - {reorderMode === "arrows" && ( + {reorderMode === "arrows" && !locked && ( <> { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } sortOrder === "custom" ? animatedMoveUp(idx) : triggerSortFlash(); }} disabled={sortOrder === "custom" && idx === 0} @@ -474,7 +509,10 @@ export const SideNavBookmarks = ({ collapse = false }) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } sortOrder === "custom" ? animatedMoveDown(idx) : triggerSortFlash(); }} disabled={sortOrder === "custom" && idx === displayBookmarks.length - 1} @@ -484,19 +522,16 @@ export const SideNavBookmarks = ({ collapse = false }) => { )} - {!(reorderMode === "drag" && locked) && ( - { - e.preventDefault(); - if (locked) { triggerLockFlash(); return; } - removeBookmark(bookmark.path); - }} - sx={{ p: "2px", ...(locked && { opacity: 0.4 }) }} - > - - - )} + { + e.preventDefault(); + removeBookmark(bookmark.path); + }} + sx={{ p: "2px" }} + > + +
  • diff --git a/src/layouts/side-nav-item.js b/src/layouts/side-nav-item.js index 2a5edd287857..2a741dfe4ee1 100644 --- a/src/layouts/side-nav-item.js +++ b/src/layouts/side-nav-item.js @@ -24,7 +24,7 @@ export const SideNavItem = (props) => { const [open, setOpen] = useState(openImmediately); const [hovered, setHovered] = useState(false); - const { handleUpdate, bookmarks = [] } = useSettings(); + const { handleUpdate, bookmarks = [], compactNav = false } = useSettings(); const isBookmarked = bookmarks.some((bookmark) => bookmark.path === path); const handleToggle = useCallback(() => { @@ -46,6 +46,7 @@ export const SideNavItem = (props) => { // Dynamic spacing and font sizing based on depth const indent = depth > 0 ? depth * 1.5 : 1; // adjust multiplication factor as needed const fontSize = depth === 0 ? 14 : 13; // top-level 14, nested 13 + const navItemPy = compactNav ? "6px" : "12px"; if (children) { return ( @@ -67,7 +68,7 @@ export const SideNavItem = (props) => { fontWeight: 500, justifyContent: "flex-start", px: `${indent * 6}px`, - py: "12px", + py: navItemPy, textAlign: "left", whiteSpace: "nowrap", width: "100%", @@ -166,9 +167,10 @@ export const SideNavItem = (props) => { textAlign: "left", whiteSpace: "nowrap", width: "calc(100% - 20px)", // Adjust the width to leave space for the bookmark icon - py: "12px", + py: navItemPy, }} {...linkProps} + onClick={(e) => e.currentTarget.blur()} > { pathname, })}
    - + , ); } else { acc.push( @@ -93,7 +93,7 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname }) => { key={item.title} path={item.path} title={item.title} - /> + />, ); } @@ -171,43 +171,49 @@ export const SideNav = (props) => { }, }} > + + {/* Bookmarks section above Dashboard */} + {showSidebarBookmarks && ( + <> + + + + )} + {/* Render all menu items */} + {renderItems({ + collapse, + depth: 0, + items: processedItems, + pathname, + })} + {" "} + {/* Add this closing tag */} + {profile?.clientPrincipal && ( - {/* Bookmarks section above Dashboard */} - {showSidebarBookmarks && ( - <> - - - - )} - {/* Render all menu items */} - {renderItems({ - collapse, - depth: 0, - items: processedItems, - pathname, - })} - {" "} - {/* Add this closing tag */} - {profile?.clientPrincipal && } - {" "} - {/* Closing tag for the parent Box */} + + + )} + {" "} + {/* Closing tag for the parent Box */} )} diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index e53973075533..370fd0f7ddbd 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -64,6 +64,7 @@ export const TopNav = (props) => { const [flashLock, setFlashLock] = useState(false); const itemRefs = useRef({}); const touchDragRef = useRef({ startIdx: null, overIdx: null }); + const tenantSelectorRef = useRef(null); const handleBookmarkClick = (event) => { setAnchorEl(event.currentTarget); @@ -148,7 +149,10 @@ export const TopNav = (props) => { if (index <= 0 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index - 1]; - if (!el1 || !el2) { moveBookmarkUp(index); return; } + if (!el1 || !el2) { + moveBookmarkUp(index); + return; + } const distance = el1.getBoundingClientRect().top - el2.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index - 1, offset1: -distance, offset2: distance }); setTimeout(() => { @@ -162,7 +166,10 @@ export const TopNav = (props) => { if (index >= bookmarks.length - 1 || animatingPair) return; const el1 = itemRefs.current[index]; const el2 = itemRefs.current[index + 1]; - if (!el1 || !el2) { moveBookmarkDown(index); return; } + if (!el1 || !el2) { + moveBookmarkDown(index); + return; + } const distance = el2.getBoundingClientRect().top - el1.getBoundingClientRect().top; setAnimatingPair({ idx1: index, idx2: index + 1, offset1: distance, offset2: -distance }); setTimeout(() => { @@ -188,9 +195,16 @@ export const TopNav = (props) => { const popoverOpen = Boolean(anchorEl); const popoverId = popoverOpen ? "bookmark-popover" : undefined; + const openSearch = useCallback(() => { + searchDialog.handleOpen(); + }, [searchDialog.handleOpen]); + useEffect(() => { const handleKeyDown = (event) => { - if ((event.metaKey || event.ctrlKey) && event.key === "k") { + if ((event.metaKey || event.ctrlKey) && event.altKey && event.key === "k") { + event.preventDefault(); + tenantSelectorRef.current?.focus(); + } else if ((event.metaKey || event.ctrlKey) && event.key === "k") { event.preventDefault(); openSearch(); } @@ -199,11 +213,7 @@ export const TopNav = (props) => { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, []); - - const openSearch = () => { - searchDialog.handleOpen(); - }; + }, [openSearch]); return ( { > - {!mdDown && } + {!mdDown && ( + + )} {mdDown && ( @@ -331,7 +343,13 @@ export const TopNav = (props) => { }, }), }} - title={sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A"} + title={ + sortOrder === "custom" + ? "Custom order" + : sortOrder === "asc" + ? "A > Z" + : "Z > A" + } > {sortOrder === "custom" && } {sortOrder === "asc" && } @@ -341,7 +359,11 @@ export const TopNav = (props) => { variant="body2" sx={{ ml: 0.5, color: "text.secondary", fontSize: 12 }} > - {sortOrder === "custom" ? "Custom order" : sortOrder === "asc" ? "A > Z" : "Z > A"} + {sortOrder === "custom" + ? "Custom order" + : sortOrder === "asc" + ? "A > Z" + : "Z > A"} @@ -355,21 +377,35 @@ export const TopNav = (props) => { displayBookmarks.map((bookmark, idx) => ( { itemRefs.current[idx] = el; }} + ref={(el) => { + itemRefs.current[idx] = el; + }} data-bookmark-index={idx} draggable={reorderMode === "drag" && sortOrder === "custom" && !locked} - {...(reorderMode === "drag" ? { - onDragStart: (e) => { - if (locked) { e.preventDefault(); triggerLockFlash(); return; } - if (sortOrder !== "custom") { e.preventDefault(); triggerSortFlash(); return; } - handleDragStart(idx); - }, - onDragEnd: handleDragEnd, - ...(sortOrder === "custom" && !locked ? { - onDragOver: (e) => handleDragOver(e, idx), - onDrop: (e) => handleDrop(e, idx), - } : {}), - } : {})} + {...(reorderMode === "drag" + ? { + onDragStart: (e) => { + if (locked) { + e.preventDefault(); + triggerLockFlash(); + return; + } + if (sortOrder !== "custom") { + e.preventDefault(); + triggerSortFlash(); + return; + } + handleDragStart(idx); + }, + onDragEnd: handleDragEnd, + ...(sortOrder === "custom" && !locked + ? { + onDragOver: (e) => handleDragOver(e, idx), + onDrop: (e) => handleDrop(e, idx), + } + : {}), + } + : {})} sx={{ color: "inherit", display: "flex", @@ -377,25 +413,34 @@ export const TopNav = (props) => { "&:hover .bookmark-controls": { opacity: 1, }, - ...(sortOrder === "custom" && reorderMode === "drag" && dragIndex === idx && { - opacity: 0.4, - }), - ...(sortOrder === "custom" && reorderMode === "drag" && dragOverIndex === idx && dragIndex !== idx && { - borderTop: "2px solid", - borderColor: "primary.main", - }), - ...(animatingPair && (animatingPair.idx1 === idx || animatingPair.idx2 === idx) && { - transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, - transition: 'transform 250ms ease-in-out', - position: 'relative', - zIndex: animatingPair.idx1 === idx ? 1 : 0, - }), + ...(sortOrder === "custom" && + reorderMode === "drag" && + dragIndex === idx && { + opacity: 0.4, + }), + ...(sortOrder === "custom" && + reorderMode === "drag" && + dragOverIndex === idx && + dragIndex !== idx && { + borderTop: "2px solid", + borderColor: "primary.main", + }), + ...(animatingPair && + (animatingPair.idx1 === idx || animatingPair.idx2 === idx) && { + transform: `translateY(${animatingPair.idx1 === idx ? animatingPair.offset1 : animatingPair.offset2}px)`, + transition: "transform 250ms ease-in-out", + position: "relative", + zIndex: animatingPair.idx1 === idx ? 1 : 0, + }), }} > {reorderMode === "drag" && !locked && ( { - if (sortOrder !== "custom") { triggerSortFlash(); return; } + if (sortOrder !== "custom") { + triggerSortFlash(); + return; + } touchDragRef.current.startIdx = idx; setDragIndex(idx); }} @@ -409,7 +454,11 @@ export const TopNav = (props) => { const li = el?.closest("[data-bookmark-index]"); if (li) { const overIdx = parseInt(li.dataset.bookmarkIndex, 10); - if (!isNaN(overIdx) && overIdx >= 0 && overIdx < (settings.bookmarks || []).length) { + if ( + !isNaN(overIdx) && + overIdx >= 0 && + overIdx < (settings.bookmarks || []).length + ) { touchDragRef.current.overIdx = overIdx; setDragOverIndex(overIdx); } @@ -470,7 +519,10 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } sortOrder === "custom" ? animatedMoveUp(idx) : triggerSortFlash(); }} disabled={sortOrder === "custom" && idx === 0} @@ -482,10 +534,17 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } - sortOrder === "custom" ? animatedMoveDown(idx) : triggerSortFlash(); + if (locked) { + triggerLockFlash(); + return; + } + sortOrder === "custom" + ? animatedMoveDown(idx) + : triggerSortFlash(); }} - disabled={sortOrder === "custom" && idx === displayBookmarks.length - 1} + disabled={ + sortOrder === "custom" && idx === displayBookmarks.length - 1 + } sx={{ opacity: sortOrder !== "custom" || locked ? 0.4 : 1 }} > @@ -497,7 +556,10 @@ export const TopNav = (props) => { size="small" onClick={(e) => { e.preventDefault(); - if (locked) { triggerLockFlash(); return; } + if (locked) { + triggerLockFlash(); + return; + } removeBookmark(bookmark.path); }} sx={{ ...(locked && { opacity: 0.4 }) }} diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index 643f03d4de39..fb575a84aae1 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -100,25 +100,52 @@ const Page = () => { }); // Watch navigation settings and apply immediately (device-local, no server save needed) - const watchedBookmarkSidebar = useWatch({ control: formcontrol.control, name: "bookmarkSidebar" }); - const watchedBookmarkPopover = useWatch({ control: formcontrol.control, name: "bookmarkPopover" }); - const watchedBookmarkReorderMode = useWatch({ control: formcontrol.control, name: "bookmarkReorderMode" }); + const watchedBookmarkSidebar = useWatch({ + control: formcontrol.control, + name: "bookmarkSidebar", + }); + const watchedBookmarkPopover = useWatch({ + control: formcontrol.control, + name: "bookmarkPopover", + }); + const watchedBookmarkReorderMode = useWatch({ + control: formcontrol.control, + name: "bookmarkReorderMode", + }); + const watchedCompactNav = useWatch({ control: formcontrol.control, name: "compactNav" }); useEffect(() => { const updates = {}; - if (watchedBookmarkSidebar !== undefined && watchedBookmarkSidebar !== settings.bookmarkSidebar) { + if ( + watchedBookmarkSidebar !== undefined && + watchedBookmarkSidebar !== settings.bookmarkSidebar + ) { updates.bookmarkSidebar = watchedBookmarkSidebar; } - if (watchedBookmarkPopover !== undefined && watchedBookmarkPopover !== settings.bookmarkPopover) { + if ( + watchedBookmarkPopover !== undefined && + watchedBookmarkPopover !== settings.bookmarkPopover + ) { updates.bookmarkPopover = watchedBookmarkPopover; } - if (watchedBookmarkReorderMode !== undefined && watchedBookmarkReorderMode !== settings.bookmarkReorderMode) { + if ( + watchedBookmarkReorderMode !== undefined && + watchedBookmarkReorderMode !== settings.bookmarkReorderMode + ) { updates.bookmarkReorderMode = watchedBookmarkReorderMode; } + if (watchedCompactNav !== undefined && watchedCompactNav !== settings.compactNav) { + updates.compactNav = watchedCompactNav; + } if (Object.keys(updates).length > 0) { settings.handleUpdate(updates); } - }, [watchedBookmarkSidebar, watchedBookmarkPopover, watchedBookmarkReorderMode]); + }, [ + watchedBookmarkSidebar, + watchedBookmarkPopover, + watchedBookmarkReorderMode, + watchedCompactNav, + ]); // Update form when initial user type is determined useEffect(() => { @@ -372,6 +399,16 @@ const Page = () => { /> ), }, + { + label: "Compact Navigation", + value: ( + + ), + }, ]} /> diff --git a/src/pages/cipp/settings/siem.js b/src/pages/cipp/settings/siem.js new file mode 100644 index 000000000000..bc5e2e70fb42 --- /dev/null +++ b/src/pages/cipp/settings/siem.js @@ -0,0 +1,192 @@ +import { + Alert, + Card, + CardContent, + CardHeader, + Container, + Divider, + Link as MuiLink, + Typography, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "../../../layouts/index.js"; +import { TabbedLayout } from "../../../layouts/TabbedLayout"; +import tabOptions from "./tabOptions"; +import CippSiemSettings from "../../../components/CippSettings/CippSiemSettings"; +import { CippCopyToClipBoard } from "../../../components/CippComponents/CippCopyToClipboard"; + +const filterExamples = [ + { + label: "Specific day", + filter: "PartitionKey eq 'YYYYMMDD'", + note: "Replace YYYYMMDD with the current date, e.g. 20260312", + }, + { + label: "Date range (last 7 days)", + filter: "PartitionKey ge '20260305' and PartitionKey le '20260312'", + note: "Use ge/le to query a range of dates", + }, +]; + +const Page = () => { + return ( + + + + + + + + + + +
    + + How Logs are Stored + + + CIPP writes all log entries to an Azure Table Storage table called{" "} + CippLogs. Each row is partitioned by date using the format{" "} + YYYYMMDD as the PartitionKey, with a unique GUID as the{" "} + RowKey. + +
    + + + + Always include a PartitionKey filter in your queries. Azure Table + Storage performs a full table scan without one, which is slow and expensive on + large tables. Use eq for a single day or ge /{" "} + le for a date range.{" "} + The date partition is in UTC time, so you may need to use a date + range to account for timezone differences. + + + +
    + + Available Columns + + +
      +
    • + PartitionKey — Date in YYYYMMDD format +
    • +
    • + RowKey — Unique log entry ID (GUID) +
    • +
    • + Timestamp — When the entry was written +
    • +
    • + Tenant — Tenant domain name +
    • +
    • + Username — User who triggered the action +
    • +
    • + API — API endpoint or function name +
    • +
    • + Message — Log message text +
    • +
    • + Severity — Log level (Info, Warning, Error, Debug) +
    • +
    • + LogData — Additional JSON data (if any) +
    • +
    • + TenantID — Tenant GUID (when available) +
    • +
    • + IP — Source IP address (when available) +
    • +
    +
    +
    + +
    + + Example $filter Queries + + + Append &$filter= to your SAS URL to filter results. Use{" "} + eq, ne, gt, lt,{" "} + ge, le, and combine with and /{" "} + or. + + {filterExamples.map((ex) => ( +
    + + {ex.label} + +
    + $filter={ex.filter} + +
    + {ex.note && ( + + {ex.note} + + )} +
    + ))} +
    + + + +
    + + Azure Tables Documentation + + +
      +
    • + + Querying Tables and Entities + + {" — "}filter syntax, operators, and supported data types +
    • +
    • + + Query Timeout and Pagination + + {" — "}handling continuation tokens for large result sets +
    • +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/settings/tabOptions.json b/src/pages/cipp/settings/tabOptions.json index 7b7dff988155..143f94ffbf49 100644 --- a/src/pages/cipp/settings/tabOptions.json +++ b/src/pages/cipp/settings/tabOptions.json @@ -30,5 +30,9 @@ { "label": "Features", "path": "/cipp/settings/features" + }, + { + "label": "SIEM", + "path": "/cipp/settings/siem" } ] \ No newline at end of file diff --git a/src/pages/tenant/administration/authentication-methods/index.js b/src/pages/tenant/administration/authentication-methods/index.js index 6bb4fb278756..2ceecbac8d62 100644 --- a/src/pages/tenant/administration/authentication-methods/index.js +++ b/src/pages/tenant/administration/authentication-methods/index.js @@ -82,7 +82,7 @@ const Page = () => { ]; const offCanvas = { - extendedInfoFields: ["id", "state", "includeTargets", "excludeTargets"], + extendedInfoFields: ["id", "displayName", "state", "includeTargets", "excludeTargets"], actions: actions, }; diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx index 9a8d54692d11..a64c30f67276 100644 --- a/src/pages/tenant/standards/templates/template.jsx +++ b/src/pages/tenant/standards/templates/template.jsx @@ -236,21 +236,47 @@ const Page = () => { }; const handleRemoveStandard = (standardName) => { - setSelectedStandards((prev) => { - const newSelected = { ...prev }; - delete newSelected[standardName]; - return newSelected; - }); - const arrayPattern = /(.*)\[(\d+)\]$/; const match = standardName.match(arrayPattern); if (match) { - const [_, baseName, index] = match; + const baseName = match[1]; + const removedIndex = parseInt(match[2]); + + // Remove the item from the form array const currentArray = formControl.getValues(baseName) || []; - const updatedArray = currentArray.filter((_, i) => i !== parseInt(index)); + const updatedArray = currentArray.filter((_, i) => i !== removedIndex); formControl.setValue(baseName, updatedArray); + + // Re-index selectedStandards to keep indices contiguous + const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const reindexPattern = new RegExp(`^${escapedBaseName}\\[(\\d+)\\]$`); + + setSelectedStandards((prev) => { + const newSelected = {}; + Object.keys(prev).forEach((key) => { + const keyMatch = key.match(reindexPattern); + if (keyMatch) { + const idx = parseInt(keyMatch[1]); + if (idx < removedIndex) { + newSelected[key] = prev[key]; + } else if (idx > removedIndex) { + // Shift higher indices down by 1 + newSelected[`${baseName}[${idx - 1}]`] = prev[key]; + } + // Skip the removed index + } else { + newSelected[key] = prev[key]; + } + }); + return newSelected; + }); } else { + setSelectedStandards((prev) => { + const newSelected = { ...prev }; + delete newSelected[standardName]; + return newSelected; + }); formControl.unregister(standardName); } };