diff --git a/.gitignore b/.gitignore index 5ee28a7a617b..97735ad0415c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ app.log # AI rules .*/rules +AGENTS.md diff --git a/package.json b/package.json index c408d29c4b78..1444ad6102ca 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,8 @@ "react-leaflet": "5.0.0", "react-leaflet-markercluster": "^5.0.0-rc.0", "react-markdown": "10.1.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^3.0.1", "react-media-hook": "^0.5.0", "react-papaparse": "^4.4.0", "react-quill": "^2.0.0", @@ -112,4 +114,4 @@ "eslint": "9.35.0", "eslint-config-next": "15.5.2" } -} \ No newline at end of file +} diff --git a/public/version.json b/public/version.json index d0e25ec56792..c9486452da33 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.5.2" -} \ No newline at end of file + "version": "8.6.0" +} diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index c069208c2dc4..67f602114c3c 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -21,6 +21,8 @@ export function ApiGetCall(props) { refetchOnReconnect = true, keepPreviousData = false, refetchInterval = false, + responseType = "json", + convertToDataUrl = false, } = props; const queryClient = useQueryClient(); const dispatch = useDispatch(); @@ -107,9 +109,22 @@ export function ApiGetCall(props) { headers: { "Content-Type": "application/json", }, + responseType: responseType, }); + + let responseData = response.data; + + // Convert blob to data URL if requested + if (convertToDataUrl && responseType === "blob" && response.data) { + responseData = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(response.data); + }); + } + if (onResult) { - onResult(response.data); // Emit each result as it arrives + onResult(responseData); // Emit each result as it arrives } if (relatedQueryKeys) { const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys]; @@ -137,7 +152,7 @@ export function ApiGetCall(props) { }); }, 1000); } - return response.data; + return responseData; } }, staleTime: staleTime, diff --git a/src/components/CippComponents/CippAddEditTenantGroups.jsx b/src/components/CippComponents/CippAddEditTenantGroups.jsx index 35696ef1fe52..9ba637d003c8 100644 --- a/src/components/CippComponents/CippAddEditTenantGroups.jsx +++ b/src/components/CippComponents/CippAddEditTenantGroups.jsx @@ -1,53 +1,96 @@ import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { Stack, Typography } from "@mui/material"; -import CippFormSection from "/src/components/CippFormPages/CippFormSection"; +import { Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippFormCondition } from "./CippFormCondition"; +import CippTenantGroupRuleBuilder from "./CippTenantGroupRuleBuilder"; -const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButtonTitle }) => { +const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButtonTitle, hideSubmitButton = false }) => { return ( - { - return { - ...values, - Action: "AddEdit", - }; - }} - initialValues={initialValues} - > + <> Properties - - - - - - + + + + + + + + + {/* Group Type Selection */} + + + + + {/* Static Group Members - Show only when Static is selected */} + + + + + + + {/* Dynamic Group Rules - Show only when Dynamic is selected */} + + + + + + + ); }; diff --git a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx new file mode 100644 index 000000000000..23fbe252ae55 --- /dev/null +++ b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx @@ -0,0 +1,378 @@ +import { useEffect, useState } from "react"; +import { Button, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState, useWatch } from "react-hook-form"; +import { PlaylistAdd } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { getCippValidator } from "../../utils/get-cipp-validator"; + +const defaultValues = { + tenantID: [], + entries: "", + notes: "", + listType: null, + listMethod: null, + NoExpiration: false, + RemoveAfter: false, +}; + +export const CippAddTenantAllowBlockListDrawer = ({ + buttonText = "Add Entry", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const formControl = useForm({ + mode: "onChange", + defaultValues, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" }); + const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" }); + const listMethod = useWatch({ control: formControl.control, name: "listMethod" }); + const listType = useWatch({ control: formControl.control, name: "listType" }); + + const isListMethodBlock = listMethod?.value === "Block"; + const isListTypeFileHash = listType?.value === "FileHash"; + const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value); + const isNoExpirationCompatible = + isListMethodBlock || (listMethod?.value === "Allow" && ["Url", "IP"].includes(listType?.value)); + + const addEntry = ApiPostCall({}); + + useEffect(() => { + if (noExpiration && formControl.getValues("RemoveAfter")) { + formControl.setValue("RemoveAfter", false, { shouldValidate: true }); + } + + if (removeAfter && formControl.getValues("NoExpiration")) { + formControl.setValue("NoExpiration", false, { shouldValidate: true }); + } + + if (isListMethodBlock && formControl.getValues("RemoveAfter")) { + formControl.setValue("RemoveAfter", false, { shouldValidate: true }); + } + + if (listType && !isListTypeSenderUrlOrFileHash && formControl.getValues("RemoveAfter")) { + formControl.setValue("RemoveAfter", false, { shouldValidate: true }); + } + + if (isListTypeFileHash && listMethod?.value !== "Block") { + formControl.setValue( + "listMethod", + { label: "Block", value: "Block" }, + { shouldValidate: true } + ); + } + + if ((listMethod || listType) && noExpiration && !isNoExpirationCompatible) { + formControl.setValue("NoExpiration", false, { shouldValidate: true }); + } + }, [ + noExpiration, + removeAfter, + isListMethodBlock, + listType, + isListTypeSenderUrlOrFileHash, + isListTypeFileHash, + isNoExpirationCompatible, + listMethod, + formControl, + ]); + + useEffect(() => { + if (addEntry.isSuccess) { + const currentTenants = formControl.getValues("tenantID"); + formControl.reset({ + ...defaultValues, + tenantID: currentTenants, + }); + } + }, [addEntry.isSuccess, formControl]); + + const validateEntries = (value) => { + if (!value) return true; + + const entries = value + .split(/[,;]/) + .map((entry) => entry.trim()) + .filter(Boolean); + const currentListType = listType?.value; + + if (currentListType === "FileHash") { + for (const entry of entries) { + if (entry.length !== 64) return "File hash entries must be exactly 64 characters"; + + const hashResult = getCippValidator(entry, "sha256"); + if (hashResult !== true) return hashResult; + } + return true; + } + + if (currentListType === "IP") { + for (const entry of entries) { + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + + if (ipv6Result !== true && ipv6CidrResult !== true) { + return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; + } + } + return true; + } + + if (currentListType === "Url") { + for (const entry of entries) { + if (entry.length > 250) { + return "URL entries must be 250 characters or less"; + } + + if (entry.includes("*") || entry.includes("~")) { + const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardUrlResult === true || wildcardDomainResult === true) { + continue; + } + + if (!/^[a-zA-Z0-9.\-*~\/]+$/.test(entry)) { + return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)"; + } + + return "Invalid wildcard format. Common formats are *.domain.com or domain.*"; + } + + const ipv4Result = getCippValidator(entry, "ip"); + const ipv4CidrResult = getCippValidator(entry, "ipv4cidr"); + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + const hostnameResult = getCippValidator(entry, "hostname"); + const urlResult = getCippValidator(entry, "url"); + + if ( + ipv4Result !== true && + ipv4CidrResult !== true && + ipv6Result !== true && + ipv6CidrResult !== true && + hostnameResult !== true && + urlResult !== true + ) { + return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; + } + } + return true; + } + + if (currentListType === "Sender") { + for (const entry of entries) { + if (entry.includes("*") || entry.includes("~")) { + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardDomainResult !== true) { + return "Invalid sender wildcard pattern. Common format is *.domain.com"; + } + continue; + } + + const senderResult = getCippValidator(entry, "senderEntry"); + if (senderResult !== true) { + return senderResult; + } + } + return true; + } + + return true; + }; + + const handleSubmit = formControl.handleSubmit((values) => { + const payload = { + tenantID: values.tenantID, + entries: values.entries, + listType: values.listType?.value, + notes: values.notes, + listMethod: values.listMethod?.value, + NoExpiration: values.NoExpiration, + RemoveAfter: values.RemoveAfter, + }; + + addEntry.mutate({ + url: "/api/AddTenantAllowBlockList", + data: payload, + }); + }); + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset(defaultValues); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default CippAddTenantAllowBlockListDrawer; diff --git a/src/components/CippComponents/CippAddTenantGroupDrawer.jsx b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx new file mode 100644 index 000000000000..75804c15f89c --- /dev/null +++ b/src/components/CippComponents/CippAddTenantGroupDrawer.jsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from "react"; +import { Button, Box } from "@mui/material"; +import { useForm, useFormState } from "react-hook-form"; +import { GroupAdd } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import CippAddEditTenantGroups from "./CippAddEditTenantGroups"; +import { getCippValidator } from "../../utils/get-cipp-validator"; + +export const CippAddTenantGroupDrawer = ({ + buttonText = "Add Tenant Group", + requiredPermissions = [], + PermissionButton = Button, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + groupType: "static", + ruleLogic: "and", + dynamicRules: [{}] + }, + }); + + const createTenantGroup = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["TenantGroupListPage"], + }); + + const { isValid, isDirty } = useFormState({ control: formControl.control }); + + useEffect(() => { + if (createTenantGroup.isSuccess) { + formControl.reset({ + groupType: "static", + ruleLogic: "and", + dynamicRules: [{}] + }); + } + }, [createTenantGroup.isSuccess]); + + const handleSubmit = (data) => { + const formattedData = { + ...data, + Action: "AddEdit", + }; + + // If it's a dynamic group, format the rules for the backend + if (data.groupType === "dynamic" && data.dynamicRules) { + formattedData.dynamicRules = data.dynamicRules.map(rule => ({ + property: rule.property?.value || rule.property, + operator: rule.operator?.value || rule.operator, + value: rule.value, + })); + formattedData.ruleLogic = data.ruleLogic || "and"; + } + + createTenantGroup.mutate({ + url: "/api/ExecTenantGroup", + data: formattedData, + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + groupType: "static", + ruleLogic: "and", + dynamicRules: [{}] + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText} + + + + + + } + > + + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/CippComponents/CippAddUserDrawer.jsx b/src/components/CippComponents/CippAddUserDrawer.jsx index a0802bde5f5c..f6a650ce1b11 100644 --- a/src/components/CippComponents/CippAddUserDrawer.jsx +++ b/src/components/CippComponents/CippAddUserDrawer.jsx @@ -8,6 +8,7 @@ import { CippApiResults } from "./CippApiResults"; import { useSettings } from "../../hooks/use-settings"; import { ApiPostCall } from "../../api/ApiCall"; import CippAddEditUser from "../CippFormPages/CippAddEditUser"; +import { Stack } from "@mui/system"; export const CippAddUserDrawer = ({ buttonText = "Add User", @@ -105,23 +106,26 @@ export const CippAddUserDrawer = ({ onClose={handleCloseDrawer} size="xl" footer={ -
- - -
+ + +
+ + +
+
} > @@ -162,7 +166,6 @@ export const CippAddUserDrawer = ({ formType="add" /> - ); diff --git a/src/components/CippComponents/CippApiLogsDrawer.jsx b/src/components/CippComponents/CippApiLogsDrawer.jsx new file mode 100644 index 000000000000..3349e891a9f5 --- /dev/null +++ b/src/components/CippComponents/CippApiLogsDrawer.jsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import { Button, Box } from "@mui/material"; +import { History } from "@mui/icons-material"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CippOffCanvas } from "./CippOffCanvas"; +import { CippDataTable } from "../CippTable/CippDataTable"; + +export const CippApiLogsDrawer = ({ + buttonText = "View API Logs", + apiFilter = null, + tenantFilter = null, + requiredPermissions = [], + PermissionButton = Button, + title = "API Logs", + ...props +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + + const handleCloseDrawer = () => { + setDrawerVisible(false); + }; + + const handleOpenDrawer = () => { + setDrawerVisible(true); + }; + + // Build the API URL with the filter + const apiUrl = `/api/ListLogs?Filter=true${apiFilter ? `&API=${apiFilter}` : ""}${ + tenantFilter ? `&Tenant=${tenantFilter}` : "" + }`; + + // Define the columns for the logs table + const simpleColumns = ["DateTime", "Severity", "Message", "User", "Tenant", "API"]; + + const actions = [ + { + label: "View Log Entry", + link: "/cipp/logs/logentry?logentry=[RowKey]", + icon: , + color: "primary", + }, + ]; + + return ( + <> + } + {...props} + > + {buttonText} + + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippGraphAttributeSelector.jsx b/src/components/CippComponents/CippGraphAttributeSelector.jsx new file mode 100644 index 000000000000..3c3d9231342f --- /dev/null +++ b/src/components/CippComponents/CippGraphAttributeSelector.jsx @@ -0,0 +1,77 @@ +import { useWatch } from "react-hook-form"; +import CippFormComponent from "./CippFormComponent"; + +/** + * A form component for selecting attributes from a Graph API endpoint + * @param {Object} props - Component props + * @param {Object} props.formControl - React Hook Form control object + * @param {string} props.name - Field name for the form + * @param {string} props.resourceFieldName - Name of the field that contains the selected resource type + * @param {string} props.label - Label for the field + * @param {string} props.helperText - Helper text for the field + * @param {boolean} props.multiple - Whether to allow multiple selections + * @param {boolean} props.required - Whether the field is required + * @param {Object} props.gridProps - Grid props to pass to the wrapper + */ +const CippGraphAttributeSelector = ({ + formControl, + name, + resourceFieldName = "DeltaResource", + label = "Attributes to Monitor", + helperText, + multiple = true, + required = false, + ...otherProps +}) => { + // Watch for changes in the resource type field + const selectedResource = useWatch({ + control: formControl.control, + name: resourceFieldName, + }); + + // Extract the value whether selectedResource is an object or string + const resourceValue = selectedResource?.value || selectedResource; + + const getHelperText = () => { + if (helperText) return helperText; + + if (!resourceValue) { + return "Select a resource type above to view available attributes"; + } + + return "Select which attributes to monitor for changes"; + }; + + const api = resourceValue + ? { + url: "/api/ListGraphRequest", + queryKey: `graph-properties-${resourceValue}`, + data: { + Endpoint: resourceValue, + ListProperties: true, + IgnoreErrors: true, + }, + labelField: (item) => item, + valueField: (item) => item, + dataKey: "Results", + } + : null; + + return ( + + ); +}; + +export default CippGraphAttributeSelector; diff --git a/src/components/CippComponents/CippGraphResourceSelector.jsx b/src/components/CippComponents/CippGraphResourceSelector.jsx new file mode 100644 index 000000000000..f2c2015c61f8 --- /dev/null +++ b/src/components/CippComponents/CippGraphResourceSelector.jsx @@ -0,0 +1,132 @@ +import { useWatch } from "react-hook-form"; +import CippFormComponent from "./CippFormComponent"; + +/** + * A form component for selecting specific resources from a Graph API endpoint + * @param {Object} props - Component props + * @param {Object} props.formControl - React Hook Form control object + * @param {string} props.name - Field name for the form + * @param {string} props.resourceFieldName - Name of the field that contains the selected resource type + * @param {string} props.label - Label for the field + * @param {string} props.helperText - Helper text for the field + * @param {boolean} props.multiple - Whether to allow multiple selections + * @param {boolean} props.required - Whether the field is required + * @param {Object} props.gridProps - Grid props to pass to the wrapper + */ +const CippGraphResourceSelector = ({ + formControl, + name, + resourceFieldName = "DeltaResource", + tenantFilterFieldName = "tenantFilter", + label = "Filter Specific Resources (Optional)", + helperText, + multiple = true, + required = false, + ...otherProps +}) => { + // Watch for changes in the resource type field + const selectedResource = useWatch({ + control: formControl.control, + name: resourceFieldName, + }); + + // Watch for changes in the tenant filter field + const tenantFilter = useWatch({ + control: formControl.control, + name: tenantFilterFieldName, + }); + + // Extract the value whether selectedResource is an object or string + const resourceValue = selectedResource?.value || selectedResource; + + // Extract the tenant filter value - handle both object and string formats + const tenantFilterValue = tenantFilter?.value || tenantFilter; + + const getHelperText = () => { + if (helperText) return helperText; + + if (!resourceValue) { + return "Select a resource type above to filter specific resources"; + } + + if ( + !tenantFilterValue || + tenantFilterValue === "AllTenants" || + (tenantFilter && typeof tenantFilter === "object" && tenantFilter.type === "Group") + ) { + return "Resource filtering is not available for All Tenants or tenant groups"; + } + + if (multiple) { + return "Optionally select specific resources to monitor (will create filter with OR statements: id eq 'id1' or id eq 'id2')"; + } + + return "Optionally select a specific resource to monitor"; + }; + + // Check if we should make the API call + const shouldFetchResources = () => { + // Must have a resource type selected + if (!resourceValue) return false; + + // Must have a tenant filter + if (!tenantFilterValue) return false; + + // Cannot be null or undefined + if (tenantFilterValue === null || tenantFilterValue === undefined) return false; + + // Cannot be AllTenants + if (tenantFilterValue === "AllTenants") return false; + + // Cannot be a tenant group (check if tenantFilter object has type: "Group") + if (tenantFilter && typeof tenantFilter === "object" && tenantFilter.type === "Group") + return false; + + return true; + }; + + const isDisabled = !resourceValue || !shouldFetchResources(); + + const api = shouldFetchResources() + ? { + url: "/api/ListGraphRequest", + queryKey: `graph-resources-${resourceValue}-${tenantFilterValue}`, + data: { + Endpoint: resourceValue, + IgnoreErrors: true, + $select: "id,displayName", + $top: 100, + tenantFilter: tenantFilterValue, + }, + labelField: (item) => item.displayName || item.id, + valueField: "id", + dataKey: "Results", + waiting: true, + } + : null; + + return ( + + ); +}; + +export default CippGraphResourceSelector; diff --git a/src/components/CippComponents/CippPolicyDeployDrawer.jsx b/src/components/CippComponents/CippPolicyDeployDrawer.jsx index 3702111ed47f..619f516496cd 100644 --- a/src/components/CippComponents/CippPolicyDeployDrawer.jsx +++ b/src/components/CippComponents/CippPolicyDeployDrawer.jsx @@ -30,7 +30,7 @@ export const CippPolicyDeployDrawer = ({ const jsonWatch = useWatch({ control: formControl.control, name: "RAWJson" }); useEffect(() => { if (CATemplates.isSuccess && watcher?.value) { - const template = CATemplates.data.find((template) => template.GUID === watcher.value); + const template = CATemplates.data?.find((template) => template.GUID === watcher.value); if (template) { const jsonTemplate = template.RAWJson ? JSON.parse(template.RAWJson) : null; setJSONData(jsonTemplate); @@ -129,7 +129,7 @@ export const CippPolicyDeployDrawer = ({ multiple={false} formControl={formControl} options={ - CATemplates.isSuccess + CATemplates.isSuccess && Array.isArray(CATemplates.data) ? CATemplates.data.map((template) => ({ label: template.Displayname, value: template.GUID, diff --git a/src/components/CippComponents/CippScheduledTaskActions.jsx b/src/components/CippComponents/CippScheduledTaskActions.jsx index 2a1ec00236ff..2df628a81cdb 100644 --- a/src/components/CippComponents/CippScheduledTaskActions.jsx +++ b/src/components/CippComponents/CippScheduledTaskActions.jsx @@ -2,7 +2,7 @@ import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; import { usePermissions } from "../../hooks/use-permissions"; -export const CippScheduledTaskActions = () => { +export const CippScheduledTaskActions = (drawerHandlers = {}) => { const { checkPermissions } = usePermissions(); const canWriteScheduler = checkPermissions(["CIPP.Scheduler.ReadWrite"]); const canReadScheduler = checkPermissions(["CIPP.Scheduler.Read", "CIPP.Scheduler.ReadWrite"]); @@ -26,20 +26,32 @@ export const CippScheduledTaskActions = () => { }, { label: "Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]", + customFunction: + drawerHandlers.openEditDrawer || + ((row) => { + // Fallback to page navigation if no drawer handler provided + window.location.href = `/cipp/scheduler/job?id=${row.RowKey}`; + }), multiPost: false, icon: , color: "success", showInActionsMenu: true, + noConfirm: true, condition: () => canWriteScheduler, }, { - label: "Clone and Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]&Clone=True", + label: "Clone Job", + customFunction: + drawerHandlers.openCloneDrawer || + ((row) => { + // Fallback to page navigation if no drawer handler provided + window.location.href = `/cipp/scheduler/job?id=${row.RowKey}&Clone=True`; + }), multiPost: false, icon: , color: "success", showInActionsMenu: true, + noConfirm: true, condition: () => canWriteScheduler, }, { diff --git a/src/components/CippComponents/CippSchedulerDrawer.jsx b/src/components/CippComponents/CippSchedulerDrawer.jsx new file mode 100644 index 000000000000..98510e77ce53 --- /dev/null +++ b/src/components/CippComponents/CippSchedulerDrawer.jsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from "react"; +import { Button, Box, Typography, Alert, AlertTitle } from "@mui/material"; +import { useForm, useFormState } from "react-hook-form"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippSchedulerForm from "../CippFormPages/CippSchedulerForm"; +import { useSettings } from "../../hooks/use-settings"; + +export const CippSchedulerDrawer = ({ + buttonText = "Add Task", + requiredPermissions = [], + PermissionButton = Button, + onSuccess, + onClose, + taskId = null, + cloneMode = false, +}) => { + const [drawerVisible, setDrawerVisible] = useState(false); + const [formKey, setFormKey] = useState(0); + const userSettingsDefaults = useSettings(); + + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + Recurrence: { value: "0", label: "Once" }, + taskType: { value: "scheduled", label: "Scheduled Task" }, + }, + }); + + const handleCloseDrawer = () => { + setDrawerVisible(false); + // Increment form key to force complete remount when reopening + setFormKey((prev) => prev + 1); + // Call onClose callback if provided (to clear parent state) + if (onClose) { + onClose(); + } + // Add a small delay before resetting to ensure drawer is closed + setTimeout(() => { + // Reset form to default values + formControl.reset({ + tenantFilter: userSettingsDefaults.currentTenant, + Recurrence: { value: "0", label: "Once" }, + taskType: { value: "scheduled", label: "Scheduled Task" }, + }); + }, 100); + }; + + const handleOpenDrawer = () => { + setDrawerVisible(true); + }; + + // Auto-open drawer if taskId is provided (for edit mode) + useEffect(() => { + if (taskId) { + setDrawerVisible(true); + } + }, [taskId]); + + return ( + <> + } + > + {buttonText} + + + + + Task Configuration + {taskId && cloneMode + ? "Clone this task with the same configuration. Modify the settings as needed and save to create a new task." + : taskId + ? "Edit the task configuration. Changes will be applied when you save." + : "Create a scheduled task or event-triggered task. Scheduled tasks run PowerShell commands at specified times, while triggered tasks respond to events like Azure AD changes."} + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippTenantGroupOffCanvas.jsx b/src/components/CippComponents/CippTenantGroupOffCanvas.jsx new file mode 100644 index 000000000000..05ed8e18f836 --- /dev/null +++ b/src/components/CippComponents/CippTenantGroupOffCanvas.jsx @@ -0,0 +1,275 @@ +import React from "react"; +import { + Box, + Typography, + Card, + CardContent, + Chip, + Alert, + AlertTitle, + useTheme, + Stack, +} from "@mui/material"; +import { Groups, Business, Rule, Info } from "@mui/icons-material"; +import { CippDataTable } from "../CippTable/CippDataTable"; + +export const CippTenantGroupOffCanvas = ({ data }) => { + const theme = useTheme(); + + if (!data) { + return ( + + No group data available + + ); + } + + const isDynamic = data.GroupType === "dynamic"; + const hasMembers = data.Members && data.Members.length > 0; + const hasDynamicRules = + data.DynamicRules && + ((Array.isArray(data.DynamicRules) && data.DynamicRules.length > 0) || + (!Array.isArray(data.DynamicRules) && Object.keys(data.DynamicRules).length > 0)); + + const renderDynamicRules = () => { + if (!hasDynamicRules) { + return ( + + No Dynamic Rules + This dynamic group has no rules configured. + + ); + } + + const operatorDisplay = { + eq: "equals", + ne: "not equals", + in: "in", + notIn: "not in", + contains: "contains", + startsWith: "starts with", + endsWith: "ends with", + }; + + // Handle both single rule object and array of rules + const rules = Array.isArray(data.DynamicRules) ? data.DynamicRules : [data.DynamicRules]; + + const renderRule = (rule, index) => ( + + + Rule {rules.length > 1 ? `${index + 1}:` : "Configuration:"} + + + Property: {rule.property} + + + Operator: {operatorDisplay[rule.operator] || rule.operator} + + + Value(s): + + {Array.isArray(rule.value) ? ( + + {rule.value.map((item, valueIndex) => ( + + ))} + + ) : ( + + )} + + ); + + const renderRulesWithLogic = () => { + if (rules.length === 1) { + return renderRule(rules[0], 0); + } + + return rules.map((rule, index) => ( + + {renderRule(rule, index)} + {index < rules.length - 1 && ( + + + + )} + + )); + }; + + return ( + + + + + Dynamic Rules + {rules.length > 1 && ( + + )} + + {renderRulesWithLogic()} + + + ); + }; + + const renderMembers = () => { + if (!hasMembers) { + return ( + + No Members + {isDynamic + ? "This dynamic group has no members that match the current rules." + : "This static group has no members assigned."} + + ); + } + + const memberColumns = ["displayName", "defaultDomainName", "customerId"]; + + return ( + + , + }} + /> + + ); + }; + + return ( + + {/* Header Section */} + + + + + + {data.Name} + + + + + ID: {data.Id} + + + + + + {data.Description && ( + + Description + {data.Description} + + )} + + + {/* Content Sections */} + + {/* Dynamic Rules Section (only for dynamic groups) */} + {isDynamic && {renderDynamicRules()}} + + {/* Members Section */} + {renderMembers()} + + {/* Additional Info */} + + + + + Additional Information + + + + + Group Type + + {isDynamic ? "Dynamic" : "Static"} + + + + Member Count + + + {data.Members?.length || 0} tenant{(data.Members?.length || 0) !== 1 ? "s" : ""} + + + {isDynamic && ( + <> + + + Rule Logic + + + {data.RuleLogic?.toUpperCase() || "AND"} + + + + + Has Rules + + {hasDynamicRules ? "Yes" : "No"} + + + )} + + + + + + ); +}; diff --git a/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx b/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx new file mode 100644 index 000000000000..2964b81a0d83 --- /dev/null +++ b/src/components/CippComponents/CippTenantGroupRuleBuilder.jsx @@ -0,0 +1,186 @@ + import React, { useState } from "react"; +import { Box, Button, IconButton, Typography, Alert, Paper } from "@mui/material"; +import { Grid } from "@mui/system"; +import { Add as AddIcon, Delete as DeleteIcon } from "@mui/icons-material"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; +import { useWatch } from "react-hook-form"; +import { + getTenantGroupPropertyOptions, + getTenantGroupOperatorOptions, + getTenantGroupValueOptions, +} from "../../utils/get-cipp-tenant-group-options"; + +const CippTenantGroupRuleBuilder = ({ formControl, name = "dynamicRules" }) => { + const [ruleCount, setRuleCount] = useState(1); + + // Watch the rules array to get current values + const watchedRules = useWatch({ + control: formControl.control, + name: name, + defaultValue: [{}], + }); + + // Watch the logic operator + const ruleLogic = useWatch({ + control: formControl.control, + name: "ruleLogic", + defaultValue: "and" + }); + + const propertyOptions = getTenantGroupPropertyOptions(); + + const addRule = () => { + const currentRules = formControl.getValues(name) || []; + const newRules = [...currentRules, {}]; + formControl.setValue(name, newRules); + setRuleCount(ruleCount + 1); + }; + + const removeRule = (index) => { + const currentRules = formControl.getValues(name) || []; + const newRules = currentRules.filter((_, i) => i !== index); + formControl.setValue(name, newRules); + setRuleCount(Math.max(1, ruleCount - 1)); + }; + + const getValueOptions = (ruleIndex) => { + const rules = watchedRules || []; + const rule = rules[ruleIndex]; + const propertyType = rule?.property?.type; + return getTenantGroupValueOptions(propertyType); + }; + + const getOperatorOptions = (ruleIndex) => { + const rules = watchedRules || []; + const rule = rules[ruleIndex]; + const propertyType = rule?.property?.type; + return getTenantGroupOperatorOptions(propertyType); + }; + + const renderRule = (ruleIndex) => { + const isFirstRule = ruleIndex === 0; + const canRemove = (watchedRules?.length || 0) > 1; + + return ( + + {!isFirstRule && ( + + {(ruleLogic || 'and').toUpperCase()} + + )} + + + {/* Property Selection */} + + + + + {/* Operator Selection */} + + + + + + + {/* Value Selection - Conditional based on property type */} + + + + + + + {/* Remove Rule Button */} + + {canRemove && ( + removeRule(ruleIndex)} size="small"> + + + )} + + + + ); + }; + + return ( + + + Dynamic Rules + + + + Define rules to automatically include tenants in this group. Rules are combined with the selected logic operator. + Example: "Available License equals Microsoft 365 E3" {(ruleLogic || 'and').toUpperCase()} "Delegated Access Status equals Direct Tenant" + + + {/* Logic Operator Selection */} + + + + + {/* Render existing rules */} + {(watchedRules || [{}]).map((_, index) => renderRule(index))} + + {/* Add Rule Button */} + + + + + ); +}; + +export default CippTenantGroupRuleBuilder; diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx index 8e3f50161694..554c72261c4b 100644 --- a/src/components/CippComponents/ScheduledTaskDetails.jsx +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -118,6 +118,31 @@ const ScheduledTaskDetails = ({ data, showActions = true }) => { isFetching={taskDetailResults.isFetching} /> + {taskDetails?.Task?.Trigger && ( + + }> + Trigger Configuration + + + { + return { + label: key, + value: getCippFormatting(value, key), + }; + })} + isFetching={taskDetailResults.isFetching} + /> + + + )} + {taskDetailResults.isFetching ? ( ) : ( diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index b7c1cdc983a2..db7c15da5af8 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -10,8 +10,9 @@ import { Typography, Tooltip, CircularProgress, + IconButton, } from "@mui/material"; -import { Check, Error } from "@mui/icons-material"; +import { Check, Error, Sync } from "@mui/icons-material"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; @@ -74,13 +75,19 @@ const CippExchangeSettingsForm = (props) => { // If this was an OOO submission, preserve the submitted values if (relatedQueryKeys.includes(`ooo-${userId}`)) { const submittedValues = formControl.getValues(); - const oooFields = ['AutoReplyState', 'InternalMessage', 'ExternalMessage', 'StartTime', 'EndTime']; - + const oooFields = [ + "AutoReplyState", + "InternalMessage", + "ExternalMessage", + "StartTime", + "EndTime", + ]; + // Reset the form formControl.reset(); - + // Restore the submitted OOO values - oooFields.forEach(field => { + oooFields.forEach((field) => { const value = submittedValues.ooo?.[field]; if (value !== undefined) { formControl.setValue(`ooo.${field}`, value); @@ -144,14 +151,14 @@ const CippExchangeSettingsForm = (props) => { cardLabelBox: { cardLabelBoxHeader: isFetching ? ( - ) : (currentSettings?.ForwardingAddress) ? ( - + ) : currentSettings?.ForwardingAddress ? ( + ) : ( - + ), }, text: "Mailbox Forwarding", - subtext: (currentSettings?.ForwardingAddress) + subtext: currentSettings?.ForwardingAddress ? "Email forwarding is configured for this mailbox" : "No email forwarding configured for this mailbox", formContent: ( @@ -171,6 +178,16 @@ const CippExchangeSettingsForm = (props) => { }, text: "Out Of Office", subtext: "Set automatic replies for when you are away", + action: oooRequest + ? { + tooltip: oooRequest.isFetching + ? "Refreshing Out Of Office data" + : "Refresh Out Of Office data", + onClick: () => oooRequest.refetch(), + disabled: oooRequest.isFetching, + isLoading: oooRequest.isFetching, + } + : null, formContent: ( @@ -190,8 +207,12 @@ const CippExchangeSettingsForm = (props) => { /> - @@ -206,8 +227,12 @@ const CippExchangeSettingsForm = (props) => { - @@ -277,7 +302,7 @@ const CippExchangeSettingsForm = (props) => { validators={{ required: "Please enter a number", min: { value: 1, message: "The minimum is 1" }, - max: { value: 1000, message: "The maximum is 1000" }, + max: { value: 1000, message: "The maximum is 1000" }, }} /> @@ -348,15 +373,49 @@ const CippExchangeSettingsForm = (props) => { - - - + + {section.action && ( + + + { + event.stopPropagation(); + section.action.onClick?.(); + }} + disabled={section.action.disabled} + sx={{ + color: "text.secondary", + }} + > + + + + + + + )} + + + + diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index 049a554b8626..50be6839a2e9 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -1,20 +1,39 @@ -import { Box, Button, Divider, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { + Box, + Button, + Divider, + Skeleton, + SvgIcon, + Typography, + ButtonGroup, + Accordion, + AccordionSummary, + AccordionDetails, + IconButton, + Alert, +} from "@mui/material"; import { Grid } from "@mui/system"; import { useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import CippGraphResourceSelector from "/src/components/CippComponents/CippGraphResourceSelector"; +import CippGraphAttributeSelector from "/src/components/CippComponents/CippGraphAttributeSelector"; import { getCippValidator } from "/src/utils/get-cipp-validator"; import { useRouter } from "next/router"; +import Link from "next/link"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import CippFormInputArray from "../CippComponents/CippFormInputArray"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { CalendarDaysIcon } from "@heroicons/react/24/outline"; +import { ExpandMoreOutlined, Delete, Add } from "@mui/icons-material"; const CippSchedulerForm = (props) => { - const { formControl, fullWidth = false } = props; // Added fullWidth prop + const { formControl, fullWidth = false, taskId = null, cloneMode = false } = props; const selectedCommand = useWatch({ control: formControl.control, name: "command" }); + const [addedConditions, setAddedConditions] = useState([{ id: 0 }]); + const [isResourcePickerDisabled, setIsResourcePickerDisabled] = useState(false); const fieldRequired = (field) => { if (field?.Required) { @@ -26,6 +45,17 @@ const CippSchedulerForm = (props) => { } }; + const handleAddCondition = () => { + setAddedConditions([...addedConditions, { id: addedConditions.length }]); + }; + + const handleRemoveCondition = (id) => { + const currentConditions = formControl.getValues("Trigger.DeltaConditions") || []; + const updatedConditions = currentConditions.filter((_, index) => index !== id); + formControl.setValue("Trigger.DeltaConditions", updatedConditions); + setAddedConditions(addedConditions.filter((condition, index) => index !== id)); + }; + const postCall = ApiPostCall({ datafromUrl: true, relatedQueryKeys: [ @@ -57,18 +87,97 @@ const CippSchedulerForm = (props) => { { value: "30d", label: "Every 30 days" }, { value: "365d", label: "Every 365 days" }, ]; + + const triggerRecurrenceOptions = [ + { value: "15m", label: "Every 15 minutes" }, + { value: "30m", label: "Every 30 minutes" }, + { value: "1h", label: "Every 1 hour" }, + { value: "4h", label: "Every 4 hours" }, + { value: "12h", label: "Every 12 hours" }, + { value: "1d", label: "Every 1 day" }, + ]; + + const taskTypeOptions = [ + { value: "scheduled", label: "Scheduled Task" }, + { value: "triggered", label: "Triggered Task" }, + ]; + + const triggerTypeOptions = [{ value: "DeltaQuery", label: "Delta Query" }]; + + const deltaResourceOptions = [ + { value: "users", label: "Users" }, + { value: "groups", label: "Groups" }, + { value: "contacts", label: "Contacts" }, + { value: "orgContact", label: "Organizational Contacts" }, + { value: "devices", label: "Devices" }, + { value: "applications", label: "Applications" }, + { value: "servicePrincipals", label: "Service Principals" }, + { value: "directoryObjects", label: "Directory Objects" }, + { value: "directoryRole", label: "Directory Roles" }, + { value: "administrativeUnits", label: "Administrative Units" }, + { value: "oAuth2PermissionGrant", label: "OAuth2 Permission Grants" }, + ]; + + const simpleEventOptions = [ + { value: "created", label: "Resource Created" }, + { value: "updated", label: "Resource Updated" }, + { value: "deleted", label: "Resource Deleted" }, + ]; + + const operatorOptions = [ + { value: "eq", label: "Equals to" }, + { value: "ne", label: "Not Equals to" }, + { value: "like", label: "Like" }, + { value: "notlike", label: "Not like" }, + { value: "notmatch", label: "Does not match" }, + { value: "gt", label: "Greater than" }, + { value: "lt", label: "Less than" }, + { value: "in", label: "In" }, + { value: "notIn", label: "Not In" }, + ]; + + // Watch for trigger-related fields + const selectedTaskType = useWatch({ control: formControl.control, name: "taskType" }); + const selectedTriggerType = useWatch({ control: formControl.control, name: "Trigger.Type" }); + const selectedDeltaResource = useWatch({ + control: formControl.control, + name: "Trigger.DeltaResource", + }); + const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + + // Watch for summary display + const selectedSimpleEvent = useWatch({ control: formControl.control, name: "Trigger.EventType" }); + const selectedRecurrence = useWatch({ control: formControl.control, name: "Recurrence" }); + const selectedScheduledTime = useWatch({ control: formControl.control, name: "ScheduledTime" }); + const selectedExecutePerResource = useWatch({ + control: formControl.control, + name: "Trigger.ExecutePerResource", + }); + const selectedDeltaExecutionMode = useWatch({ + control: formControl.control, + name: "Trigger.ExecutionMode", + }); + const selectedUseConditions = useWatch({ + control: formControl.control, + name: "Trigger.UseConditions", + }); + const selectedDeltaConditions = useWatch({ + control: formControl.control, + name: "Trigger.DeltaConditions", + }); const commands = ApiGetCall({ url: "/api/ListFunctionParameters?Module=CIPPCore", queryKey: "ListCommands", }); const router = useRouter(); + const scheduledTaskList = ApiGetCall({ url: "/api/ListScheduledItems", - queryKey: "ListScheduledItems-Edit-" + router.query.id, - waiting: !!router.query.id, + queryKey: "ListScheduledItems-Edit-" + (taskId || router.query.id), + waiting: !!(taskId || router.query.id), data: { - Id: router.query.id, + Id: taskId || router.query.id, }, }); @@ -76,19 +185,113 @@ const CippSchedulerForm = (props) => { url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-AllTenants", }); + + // Check if resource picker should be disabled useEffect(() => { - if (scheduledTaskList.isSuccess && router.query.id) { - const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + console.log(selectedTenant); + if (!selectedTenant) { + setIsResourcePickerDisabled(false); + return; + } + + // Disable if AllTenants is selected + if (selectedTenant.value === "AllTenants") { + setIsResourcePickerDisabled(true); + return; + } + + // Disable if a tenant group is selected (groups have type: "Group") + if (selectedTenant.type === "Group") { + setIsResourcePickerDisabled(true); + return; + } + + setIsResourcePickerDisabled(false); + }, [selectedTenant]); + + // Helper functions for accordion summaries + const getTriggerSummary = () => { + if (!selectedTriggerType || selectedTaskType?.value !== "triggered") return ""; + + let summary = selectedTriggerType.label; + + if (selectedTriggerType.value === "DeltaQuery") { + if (selectedDeltaResource?.label) { + summary += ` - ${selectedDeltaResource.label}`; + } + if (selectedSimpleEvent?.label) { + summary += ` (${selectedSimpleEvent.label})`; + } + if (selectedUseConditions && selectedDeltaConditions?.length > 0) { + summary += ` with ${selectedDeltaConditions.length} condition${ + selectedDeltaConditions.length > 1 ? "s" : "" + }`; + } + } + + return summary; + }; + + const getScheduleSummary = () => { + if (selectedTaskType?.value !== "scheduled") return ""; + + let summary = ""; + if (selectedScheduledTime) { + // Handle both Unix timestamp and regular date formats + let date; + if ( + typeof selectedScheduledTime === "number" || + (typeof selectedScheduledTime === "string" && /^\d+$/.test(selectedScheduledTime)) + ) { + // Unix timestamp (seconds or milliseconds) + const timestamp = parseInt(selectedScheduledTime); + date = new Date(timestamp > 1000000000000 ? timestamp : timestamp * 1000); + } else { + date = new Date(selectedScheduledTime); + } + // Include both date and time + summary += `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + } + if (selectedRecurrence) { + summary += summary ? ` - ${selectedRecurrence.label}` : selectedRecurrence.label; + } + + return summary; + }; + + const getCommandSummary = () => { + if (!selectedCommand) return ""; + + let summary = selectedCommand.label; + + if (selectedTaskType?.value === "triggered" && selectedTriggerType?.value === "DeltaQuery") { + if (selectedExecutePerResource) { + summary += " (per resource)"; + } + if (selectedDeltaExecutionMode) { + summary += ` - ${selectedDeltaExecutionMode.label}`; + } + } + + return summary; + }; + useEffect(() => { + if (scheduledTaskList.isSuccess && (taskId || router.query.id)) { + const task = scheduledTaskList.data.find( + (task) => task.RowKey === (taskId || router.query.id) + ); // Early return if task is not found if (!task) { - console.warn(`Task with RowKey ${router.query.id} not found`); + console.warn(`Task with RowKey ${taskId || router.query.id} not found`); return; } - const postExecution = task?.postExecution?.split(",").map((item) => { - return { label: item, value: item }; - }); + const postExecution = task?.PostExecution + ? task.PostExecution.split(",").map((item) => { + return { label: item.trim(), value: item.trim() }; + }) + : []; // Find tenantFilter in tenantList, and create a label/value pair for the autocomplete if (tenantList.isSuccess) { @@ -180,13 +383,29 @@ const CippSchedulerForm = (props) => { const ResetParams = { tenantFilter: tenantFilterForForm, - RowKey: router.query.Clone ? null : task.RowKey, - Name: router.query.Clone ? `${task.Name} (Clone)` : task?.Name, + RowKey: router.query.Clone || cloneMode ? null : task.RowKey, + Name: router.query.Clone || cloneMode ? `${task.Name} (Clone)` : task?.Name, command: { label: task.Command, value: task.Command, addedFields: commandForForm }, ScheduledTime: task.ScheduledTime, Recurrence: recurrence, parameters: task.Parameters, postExecution: postExecution, + // Set task type based on whether trigger exists + taskType: task.Trigger + ? { value: "triggered", label: "Triggered Task" } + : { value: "scheduled", label: "Scheduled Task" }, + // Trigger configuration - use the trigger data directly since it's already in the correct format + ...(task.Trigger && { + "Trigger.Type": task.Trigger.Type, + "Trigger.DeltaResource": task.Trigger.DeltaResource, + "Trigger.EventType": task.Trigger.EventType, + "Trigger.ResourceFilter": task.Trigger.ResourceFilter || [], + "Trigger.WatchedAttributes": task.Trigger.WatchedAttributes || [], + "Trigger.UseConditions": task.Trigger.UseConditions || false, + "Trigger.DeltaConditions": task.Trigger.DeltaConditions || [], + "Trigger.ExecutePerResource": task.Trigger.ExecutePerResource || false, + "Trigger.ExecutionMode": task.Trigger.ExecutionMode, + }), // Show advanced parameters if: // 1. RawJsonParameters exist // 2. It's a system command with no defined parameters @@ -200,14 +419,32 @@ const CippSchedulerForm = (props) => { RawJsonParameters: task.RawJsonParameters || "", }; formControl.reset(ResetParams); + + // Set up condition builder if task has delta conditions + if ( + task.Trigger?.DeltaConditions && + Array.isArray(task.Trigger.DeltaConditions) && + task.Trigger.DeltaConditions.length > 0 + ) { + const conditionsWithIds = task.Trigger.DeltaConditions.map((condition, index) => ({ + id: index, + ...condition, + })); + setAddedConditions(conditionsWithIds); + } else { + // Reset to default single condition if no conditions exist + setAddedConditions([{ id: 0 }]); + } } } } }, [ + taskId, router.query.id, scheduledTaskList.isSuccess, tenantList.isSuccess, router.query.Clone, + cloneMode, commands.isSuccess, ]); @@ -230,8 +467,10 @@ const CippSchedulerForm = (props) => { // Get the original task parameters if we're editing (to preserve complex objects) let parametersToUse = null; - if (router.query.id && scheduledTaskList.isSuccess) { - const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + if ((taskId || router.query.id) && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find( + (task) => task.RowKey === (taskId || router.query.id) + ); if (task?.Parameters) { parametersToUse = task.Parameters; } @@ -258,7 +497,7 @@ const CippSchedulerForm = (props) => { formControl.setValue("RawJsonParameters", "{}"); } } - }, [advancedParameters, router.query.id, scheduledTaskList.isSuccess]); + }, [advancedParameters, taskId, router.query.id, scheduledTaskList.isSuccess]); const gridSize = fullWidth ? 12 : 4; // Adjust size based on fullWidth prop @@ -268,6 +507,7 @@ const CippSchedulerForm = (props) => { {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( )} + {/* Top section: Tenant and Task Name */} { name="Name" label="Task Name" formControl={formControl} - /> - - - - { - const baseOptions = - commands.data?.map((command) => { - return { - label: command.Function, - value: command.Function, - addedFields: command, - }; - }) || []; - - // If we're editing a task and the command isn't in the base options, add it - if (router.query.id && scheduledTaskList.isSuccess) { - const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); - if (task?.Command && !baseOptions.find((opt) => opt.value === task.Command)) { - baseOptions.unshift({ - label: task.Command, - value: task.Command, - addedFields: { - Function: task.Command, - Parameters: [], - }, - }); - } - } - - return baseOptions; - })()} - validators={{ - validate: (value) => { - if (!value) { - return "Please select a Command"; - } - return true; - }, - }} - /> - - - - + + { - let options = [...recurrenceOptions]; - - // If we're editing a task and the recurrence isn't in the base options, add it - if (router.query.id && scheduledTaskList.isSuccess) { - const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); - if (task?.Recurrence && !options.find((opt) => opt.value === task.Recurrence)) { - options.push({ - value: task.Recurrence, - label: `Custom: ${task.Recurrence}`, - }); - } - } - - return options; - })()} - multiple={false} - disableClearable={true} - creatable={true} + multiple + creatable={false} + options={[ + { label: "Webhook", value: "Webhook" }, + { label: "Email", value: "Email" }, + { label: "PSA", value: "PSA" }, + ]} /> - {selectedCommand?.addedFields?.Synopsis && ( - - - PowerShell Command: - - {selectedCommand.addedFields.Synopsis} - - - - )} - {selectedCommand?.addedFields?.Parameters?.map((param, idx) => ( - - - {param.Type === "System.Boolean" || - param.Type === "System.Management.Automation.SwitchParameter" ? ( - - ) : param.Type === "System.Collections.Hashtable" ? ( - - ) : param.Type?.startsWith("System.String") ? ( - - ) : ( - - )} - - - ))} + {/* Divider */} - + + + {/* Task Type Selection */} - + + {taskTypeOptions.map((option) => ( + + ))} + + + {/* Trigger Configuration Accordion */} - getCippValidator(value, "json"), - }} - formControl={formControl} - multiline - rows={6} - maxRows={30} - sx={{ - "& .MuiInputBase-root": { - overflow: "auto", - minHeight: "200px", - }, - }} - placeholder={`Enter a JSON object`} - /> + + }> + + Trigger Configuration + {getTriggerSummary() && ( + + - {getTriggerSummary()} + + )} + + + + + + + + + {/* Delta Query Configuration */} + + + + + Delta queries track changes to Microsoft Graph resources. Learn more about{" "} + + delta query concepts and usage + {" "} + in the Microsoft documentation. + + + + + + + + + + + + + + + + + + + + + + + {/* Condition Builder for all event types */} + + + + + + + + + Delta Query Conditions + + + + Create PowerShell-style Where-Object conditions to filter delta query + results. Each condition compares a resource property against a specific + value. Multiple conditions work as AND logic - all must be true to trigger + the task. + + + + {addedConditions.map((condition, index) => ( + + + + + + + + + + + + handleRemoveCondition(index)} color="error"> + + + + + ))} + + + {/* Delta Query Execution Options */} + + + + + + + + + {/* Trigger Recurrence */} + + + + + + + + + + + {/* Schedule Configuration - Only for scheduled tasks */} + + + + }> + + Schedule Configuration + {getScheduleSummary() && ( + + - {getScheduleSummary()} + + )} + + + + + + + + + { + let options = [...recurrenceOptions]; + + // If we're editing a task and the recurrence isn't in the base options, add it + if ((taskId || router.query.id) && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find( + (task) => task.RowKey === (taskId || router.query.id) + ); + if ( + task?.Recurrence && + !options.find((opt) => opt.value === task.Recurrence) + ) { + options.push({ + value: task.Recurrence, + label: `Custom: ${task.Recurrence}`, + }); + } + } + + return options; + })()} + multiple={false} + disableClearable={true} + creatable={true} + /> + + + + + + {/* Command & Parameters - For both scheduled and triggered tasks */} - + + }> + + Command & Parameters + {getCommandSummary() && ( + + - {getCommandSummary()} + + )} + + + + + {/* Command selection for both scheduled and triggered tasks */} + + { + const baseOptions = + commands.data?.map((command) => { + return { + label: command.Function, + value: command.Function, + addedFields: command, + }; + }) || []; + + // If we're editing a task and the command isn't in the base options, add it + if ((taskId || router.query.id) && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find( + (task) => task.RowKey === (taskId || router.query.id) + ); + if ( + task?.Command && + !baseOptions.find((opt) => opt.value === task.Command) + ) { + baseOptions.unshift({ + label: task.Command, + value: task.Command, + addedFields: { + Function: task.Command, + Parameters: [], + }, + }); + } + } + + return baseOptions; + })()} + validators={{ + validate: (value) => { + if (!value) { + return "Please select a Command"; + } + return true; + }, + }} + /> + + + {selectedCommand?.addedFields?.Synopsis && ( + + + PowerShell Command: + + {selectedCommand.addedFields.Synopsis} + + + + )} + + {selectedCommand?.addedFields?.Parameters?.map((param, idx) => ( + + + {param.Type === "System.Boolean" || + param.Type === "System.Management.Automation.SwitchParameter" ? ( + + ) : param.Type === "System.Collections.Hashtable" ? ( + + ) : param.Type?.startsWith("System.String") ? ( + + ) : ( + + )} + + + ))} + + + + + + + + getCippValidator(value, "json"), + }} + formControl={formControl} + multiline + rows={6} + maxRows={30} + sx={{ + "& .MuiInputBase-root": { + overflow: "auto", + minHeight: "200px", + }, + }} + placeholder={`Enter a JSON object`} + /> + + + + + + diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 7c4590fc8cee..09cc642f61bd 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -418,9 +418,9 @@ const CippStandardsSideBar = ({ {/* Show drift error */} {isDriftMode && driftError && {driftError}} - {watchForm.tenantFilter?.some( + {(watchForm.tenantFilter?.some( (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" - ) && ( + ) || (watchForm.excludedTenants && watchForm.excludedTenants.length > 0)) && ( <> { {cardButton || !hideTitle ? ( <> - + ) : null} diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 34db29fd7a1c..5b067cf4e7c3 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -69,10 +69,14 @@ export const PrivateRoute = ({ children, routeType }) => { const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? []; const isAuthenticated = userRoles.length > 0 && !apiRoles?.error; const isAdmin = roles?.includes("admin") || roles?.includes("superadmin"); - if (routeType === "admin") { - return !isAdmin ? : children; - } else { - return !isAuthenticated ? : children; + if (routeType === "admin" && !isAdmin) { + return ; } + + if (!isAuthenticated) { + return ; + } + + return children; } }; diff --git a/src/components/ReleaseNotesDialog.js b/src/components/ReleaseNotesDialog.js new file mode 100644 index 000000000000..6fc9274dbca3 --- /dev/null +++ b/src/components/ReleaseNotesDialog.js @@ -0,0 +1,474 @@ +import { + Component, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Link, + Stack, + Typography, +} from "@mui/material"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import rehypeRaw from "rehype-raw"; +import { unified } from "unified"; +import packageInfo from "../../public/version.json"; +import { ApiGetCall } from "../api/ApiCall"; +import { GitHub } from "@mui/icons-material"; +import { CippAutoComplete } from "./CippComponents/CippAutocomplete"; + +const RELEASE_COOKIE_KEY = "cipp_release_notice"; +const RELEASE_OWNER = "KelvinTegelaar"; +const RELEASE_REPO = "CIPP"; + +const secureFlag = () => { + if (typeof window === "undefined") { + return ""; + } + + return window.location.protocol === "https:" ? " Secure" : ""; +}; + +const getCookie = (name) => { + if (typeof document === "undefined") { + return null; + } + + const cookiePrefix = `${name}=`; + const cookies = document.cookie.split("; "); + + for (const cookie of cookies) { + if (cookie.startsWith(cookiePrefix)) { + return decodeURIComponent(cookie.slice(cookiePrefix.length)); + } + } + + return null; +}; + +const setCookie = (name, value, days = 365) => { + if (typeof document === "undefined") { + return; + } + + const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString(); + document.cookie = `${name}=${encodeURIComponent( + value + )}; expires=${expires}; path=/; SameSite=Lax;${secureFlag()}`; +}; + +const buildReleaseMetadata = (version) => { + const [major = "0", minor = "0", patch = "0"] = String(version).split("."); + const currentTag = `v${major}.${minor}.${patch}`; + const baseTag = `v${major}.${minor}.0`; + const tagToUse = patch === "0" ? currentTag : baseTag; + + return { + currentTag, + releaseTag: tagToUse, + releaseUrl: `https://github.com/${RELEASE_OWNER}/${RELEASE_REPO}/releases/tag/${tagToUse}`, + }; +}; + +const formatReleaseBody = (body) => { + if (!body) { + return ""; + } + + return body.replace(/(^|[^\w/])@([a-zA-Z0-9-]+)/g, (match, prefix, username) => { + return `${prefix}[@${username}](https://github.com/${username})`; + }); +}; + +class MarkdownErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error) { + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("Failed to render release notes", error); + } + } + + render() { + if (this.state.hasError) { + return this.props.fallback(this.state.error); + } + + return this.props.children; + } +} + +export const ReleaseNotesDialog = forwardRef((_props, ref) => { + const releaseMeta = useMemo(() => buildReleaseMetadata(packageInfo.version), []); + const [isEligible, setIsEligible] = useState(false); + const [open, setOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [manualOpenRequested, setManualOpenRequested] = useState(false); + const [selectedReleaseTag, setSelectedReleaseTag] = useState(releaseMeta.releaseTag); + const hasOpenedRef = useRef(false); + + useEffect(() => { + hasOpenedRef.current = false; + }, [releaseMeta.releaseTag]); + + useEffect(() => { + setSelectedReleaseTag(releaseMeta.releaseTag); + }, [releaseMeta.releaseTag]); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const storedValue = getCookie(RELEASE_COOKIE_KEY); + + if (storedValue !== releaseMeta.releaseTag) { + setIsEligible(true); + } + }, [releaseMeta.releaseTag]); + + const shouldFetchReleaseList = isEligible || manualOpenRequested || open; + + const releaseListQuery = ApiGetCall({ + url: "/api/ListGitHubReleaseNotes", + queryKey: "list-github-release-options", + data: { + Owner: RELEASE_OWNER, + Repository: RELEASE_REPO, + }, + waiting: shouldFetchReleaseList, + staleTime: 300000, + }); + + const isReleaseListLoading = releaseListQuery.isLoading || releaseListQuery.isFetching; + + const releaseCatalog = useMemo(() => { + return Array.isArray(releaseListQuery.data) ? releaseListQuery.data : []; + }, [releaseListQuery.data]); + + useEffect(() => { + if (!releaseCatalog.length) { + return; + } + + if (!selectedReleaseTag) { + setSelectedReleaseTag(releaseCatalog[0].releaseTag); + return; + } + + const hasSelected = releaseCatalog.some((release) => release.releaseTag === selectedReleaseTag); + + if (!hasSelected) { + const fallbackRelease = + releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) || + releaseCatalog[0]; + if (fallbackRelease) { + setSelectedReleaseTag(fallbackRelease.releaseTag); + } + } + }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]); + + const releaseOptions = useMemo(() => { + const mapped = releaseCatalog.map((release) => { + const tag = release.releaseTag ?? release.tagName; + const label = release.name ? `${release.name} (${tag})` : tag; + return { + label, + value: tag, + addedFields: { + htmlUrl: release.htmlUrl, + publishedAt: release.publishedAt, + }, + }; + }); + + if (selectedReleaseTag && !mapped.some((option) => option.value === selectedReleaseTag)) { + mapped.push({ + label: selectedReleaseTag, + value: selectedReleaseTag, + addedFields: { + htmlUrl: releaseMeta.releaseUrl, + publishedAt: null, + }, + }); + } + + return mapped; + }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseUrl]); + + const selectedReleaseValue = useMemo(() => { + if (!selectedReleaseTag) { + return null; + } + + return ( + releaseOptions.find((option) => option.value === selectedReleaseTag) || { + label: selectedReleaseTag, + value: selectedReleaseTag, + } + ); + }, [releaseOptions, selectedReleaseTag]); + + const handleReleaseChange = useCallback( + (newValue) => { + const nextValue = Array.isArray(newValue) ? newValue[0] : newValue; + if (nextValue?.value && nextValue.value !== selectedReleaseTag) { + setSelectedReleaseTag(nextValue.value); + } + }, + [selectedReleaseTag] + ); + + useImperativeHandle(ref, () => ({ + open: () => { + setManualOpenRequested(true); + setOpen(true); + }, + })); + + const selectedReleaseData = useMemo(() => { + if (!selectedReleaseTag) { + return null; + } + + return ( + releaseCatalog.find((release) => release.releaseTag === selectedReleaseTag) || + releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) || + null + ); + }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]); + + const handleDismissUntilNextRelease = () => { + const newestRelease = releaseCatalog[0]; + const tagToStore = + newestRelease?.releaseTag ?? newestRelease?.tagName ?? releaseMeta.releaseTag; + setCookie(RELEASE_COOKIE_KEY, tagToStore); + setOpen(false); + setIsExpanded(false); + setManualOpenRequested(false); + setIsEligible(false); + }; + + const handleRemindLater = () => { + setOpen(false); + setIsExpanded(false); + setManualOpenRequested(false); + }; + + const toggleExpanded = () => { + setIsExpanded((prev) => !prev); + }; + + const requestedVersionLabel = + selectedReleaseData?.releaseTag ?? selectedReleaseTag ?? releaseMeta.currentTag; + const releaseName = + selectedReleaseData?.name || selectedReleaseValue?.label || `CIPP ${releaseMeta.currentTag}`; + const releaseHeading = releaseName || requestedVersionLabel; + const releaseBody = typeof selectedReleaseData?.body === "string" ? selectedReleaseData.body : ""; + const releaseUrl = + selectedReleaseData?.htmlUrl ?? + selectedReleaseValue?.addedFields?.htmlUrl ?? + releaseMeta.releaseUrl; + const formattedReleaseBody = useMemo(() => formatReleaseBody(releaseBody), [releaseBody]); + const gfmSupport = useMemo(() => { + if (!formattedReleaseBody) { + return { plugins: [remarkGfm], error: null }; + } + + try { + unified().use(remarkParse).use(remarkGfm).parse(formattedReleaseBody); + return { plugins: [remarkGfm], error: null }; + } catch (err) { + return { plugins: [], error: err }; + } + }, [formattedReleaseBody]); + + useEffect(() => { + if (!isEligible || hasOpenedRef.current) { + return; + } + + if (releaseCatalog.length || releaseListQuery.error) { + setOpen(true); + hasOpenedRef.current = true; + } + }, [isEligible, releaseCatalog.length, releaseListQuery.error]); + + return ( + + + + + {`Release notes for ${releaseHeading}`} + + + + + + + + {releaseListQuery.error ? ( + + We couldn't load additional releases right now. The latest release notes are shown + below. + {releaseListQuery.error?.message ? ` (${releaseListQuery.error.message})` : ""} + + ) : null} + {gfmSupport.error ? ( + + Displaying these release notes without GitHub-flavoured markdown enhancements due to a + parsing issue. Formatting may look different. + + ) : null} + {isReleaseListLoading && !selectedReleaseData ? ( + + + + ) : releaseListQuery.error ? ( + + We couldn't load the release notes right now. You can view them on GitHub instead. + {releaseListQuery.error?.message ? ` (${releaseListQuery.error.message})` : ""} + + ) : ( + + ( + + + We couldn't format these release notes + {error?.message ? ` (${error.message})` : ""}. A plain-text version is shown + below. + + + {releaseBody} + + + )} + > + ( + + ), + img: ({ node, ...props }) => ( + + ), + }} + rehypePlugins={[rehypeRaw]} + remarkPlugins={gfmSupport.plugins} + > + {formattedReleaseBody} + + + + )} + + + + + + + + + + + ); +}); + +ReleaseNotesDialog.displayName = "ReleaseNotesDialog"; diff --git a/src/contexts/release-notes-context.js b/src/contexts/release-notes-context.js new file mode 100644 index 000000000000..54f29623522e --- /dev/null +++ b/src/contexts/release-notes-context.js @@ -0,0 +1,30 @@ +import { createContext, useCallback, useContext, useMemo, useRef } from "react"; +import PropTypes from "prop-types"; +import { ReleaseNotesDialog } from "../components/ReleaseNotesDialog"; + +const ReleaseNotesContext = createContext({ + openReleaseNotes: () => {}, +}); + +export const ReleaseNotesProvider = ({ children }) => { + const dialogRef = useRef(null); + + const openReleaseNotes = useCallback(() => { + dialogRef.current?.open(); + }, []); + + const value = useMemo(() => ({ openReleaseNotes }), [openReleaseNotes]); + + return ( + + {children} + + + ); +}; + +ReleaseNotesProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useReleaseNotes = () => useContext(ReleaseNotesContext); diff --git a/src/data/alerts.json b/src/data/alerts.json index 7ca2114492e7..082fe856f297 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -213,6 +213,12 @@ "recommendedRunInterval": "7d", "description": "Monitors domain security scores from the DomainAnalyser and alerts when scores fall below the specified threshold." }, + { + "name": "MXRecordChanged", + "label": "Alert on MX record changes", + "recommendedRunInterval": "1d", + "description": "Monitors MX records for all domains and alerts when changes are detected. This helps identify potential mail routing changes that could indicate security issues or unauthorized modifications." + }, { "name": "GlobalAdminNoAltEmail", "label": "Alert on Global Admin accounts without alternate email address", diff --git a/src/data/standards.json b/src/data/standards.json index d146bf5b9a7a..c29a094ed366 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -912,6 +912,39 @@ "powershellEquivalent": "Update-MgPolicyAuthorizationPolicy", "recommendedBy": ["CIS", "CIPP"] }, + { + "name": "standards.BitLockerKeysForOwnedDevice", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", + "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", + "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select state", + "name": "standards.BitLockerKeysForOwnedDevice.state", + "options": [ + { + "label": "Restrict", + "value": "restrict" + }, + { + "label": "Allow", + "value": "allow" + } + ] + } + ], + "label": "Control BitLocker key recovery for owned devices", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-10-12", + "powershellEquivalent": "Update-MgBetaPolicyAuthorizationPolicy", + "recommendedBy": [] + }, { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index cc0da2049060..579780b6adc8 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -24,7 +24,8 @@ import { import { usePopover } from "../hooks/use-popover"; import { paths } from "../paths"; import { ApiGetCall } from "../api/ApiCall"; -import { CogIcon } from "@heroicons/react/24/outline"; +import { CogIcon, DocumentTextIcon } from "@heroicons/react/24/outline"; +import { useReleaseNotes } from "../contexts/release-notes-context"; import { useQueryClient } from "@tanstack/react-query"; export const AccountPopover = (props) => { @@ -39,11 +40,25 @@ export const AccountPopover = (props) => { const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); const popover = usePopover(); const queryClient = useQueryClient(); + const { openReleaseNotes } = useReleaseNotes(); const orgData = ApiGetCall({ url: "/api/me", queryKey: "authmecipp", }); + const userDetails = orgData.data?.clientPrincipal?.userDetails; + + // Cache user photo with user-specific key + const userPhoto = ApiGetCall({ + url: "/api/ListUserPhoto", + data: { UserID: userDetails }, + queryKey: `userPhoto-${userDetails}`, + waiting: !!userDetails, + staleTime: Infinity, + responseType: "blob", + convertToDataUrl: true, + }); + const handleLogout = useCallback(async () => { try { popover.handleClose(); @@ -63,15 +78,12 @@ export const AccountPopover = (props) => { sx={{ height: 40, width: 40, + fontSize: 20, }} variant="rounded" - src={ - orgData.data?.clientPrincipal?.userDetails - ? `/api/ListUserPhoto?UserID=${orgData.data?.clientPrincipal?.userDetails}` - : "" - } + src={userPhoto.data && !userPhoto.isError ? userPhoto.data : undefined} > - {orgData.data?.userDetails?.[0] || ""} + {userDetails?.[0]?.toUpperCase() || ""} ); @@ -152,6 +164,19 @@ export const AccountPopover = (props) => { + { + popover.handleClose(); + openReleaseNotes(); + }} + > + + + + + + + diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index 47c601fa3027..772671f5e3db 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -135,6 +135,11 @@ export const SideNav = (props) => { imagesrc: "/sponsors/huntress_teal.png", priority: 1, }, + { + link: "https://rightofboom.com/rob-2026-overview/rob-2026-registration/?utm_source=CIPP&utm_medium=referral&utm_campaign=CIPPM365&utm_content=cta_button", + imagesrc: theme === "light" ? "/sponsors/RoB-light.png" : "/sponsors/RoB.png", + priority: 1, + }, ]; const randomSponsorImage = () => { @@ -237,7 +242,7 @@ export const SideNav = (props) => { cursor: "pointer", maxHeight: "50px", // Limit the height of the image width: "auto", - maxWidth: "100px" // Maintain aspect ratio with max width + maxWidth: "100px", // Maintain aspect ratio with max width }} onClick={() => window.open(randomimg.link)} /> diff --git a/src/pages/_app.js b/src/pages/_app.js index 3623d863d18a..b7af5f4a3bcc 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -4,6 +4,7 @@ import { Provider as ReduxProvider } from "react-redux"; import { CacheProvider } from "@emotion/react"; import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; +import { ReleaseNotesProvider } from "../contexts/release-notes-context"; import { SettingsConsumer, SettingsProvider } from "../contexts/settings-context"; import { RTL } from "../components/rtl"; import { store } from "../store"; @@ -298,7 +299,9 @@ const App = (props) => { - {getLayout()} + + {getLayout()} + { - const actions = CippScheduledTaskActions(); + const [editTaskId, setEditTaskId] = useState(null); + const [cloneTaskId, setCloneTaskId] = useState(null); + + const drawerHandlers = { + openEditDrawer: (row) => { + setEditTaskId(row.RowKey); + }, + openCloneDrawer: (row) => { + setCloneTaskId(row.RowKey); + }, + }; + + const actions = CippScheduledTaskActions(drawerHandlers); const filterList = [ { @@ -40,39 +52,62 @@ const Page = () => { }; const [showHiddenJobs, setShowHiddenJobs] = useState(false); return ( - - - - - } - tenantInTitle={false} - title="Scheduled Tasks" - apiUrl={ - showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems" - } - queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`} - simpleColumns={[ - "ExecutedTime", - "TaskState", - "Tenant", - "Name", - "ScheduledTime", - "Command", - "Parameters", - "PostExecution", - "Recurrence", - "Results", - ]} - actions={actions} - offCanvas={offCanvas} - filters={filterList} - /> + <> + + + + + } + tenantInTitle={false} + title="Scheduled Tasks" + apiUrl={ + showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems" + } + queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`} + simpleColumns={[ + "ExecutedTime", + "TaskState", + "Tenant", + "Name", + "ScheduledTime", + "Command", + "Parameters", + "PostExecution", + "Recurrence", + "Results", + ]} + actions={actions} + offCanvas={offCanvas} + filters={filterList} + /> + + {/* Edit Drawer */} + {editTaskId && ( + setEditTaskId(null)} + onClose={() => setEditTaskId(null)} + PermissionButton={({ children }) => <>{children}} + /> + )} + + {/* Clone Drawer */} + {cloneTaskId && ( + setCloneTaskId(null)} + onClose={() => setCloneTaskId(null)} + PermissionButton={({ children }) => <>{children}} + /> + )} + ); }; diff --git a/src/pages/cipp/settings/partner-webhooks.js b/src/pages/cipp/settings/partner-webhooks.js index 67d218fb22c6..f5ac4dd87889 100644 --- a/src/pages/cipp/settings/partner-webhooks.js +++ b/src/pages/cipp/settings/partner-webhooks.js @@ -27,7 +27,7 @@ import { useState } from "react"; import { Close } from "@mui/icons-material"; const Page = () => { - const pageTitle = "Partner Webhooks"; + const pageTitle = "Automated onboarding"; const [testRunning, setTestRunning] = useState(false); const [correlationId, setCorrelationId] = useState(null); const [validateRunning, setValidateRunning] = useState(false); diff --git a/src/pages/email/administration/tenant-allow-block-lists/add.jsx b/src/pages/email/administration/tenant-allow-block-lists/add.jsx deleted file mode 100644 index ad8fda8cd4f6..000000000000 --- a/src/pages/email/administration/tenant-allow-block-lists/add.jsx +++ /dev/null @@ -1,300 +0,0 @@ -import { useEffect } from "react"; -import "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { useSettings } from "../../../../hooks/use-settings"; -import { getCippValidator } from "/src/utils/get-cipp-validator"; - -const AddTenantAllowBlockList = () => { - const tenantDomain = useSettings().currentTenant; - - const formControl = useForm({ - mode: "onChange", - defaultValues: { - entries: "", - notes: "", - listType: null, - listMethod: null, - NoExpiration: false, - RemoveAfter: false, - }, - }); - - const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" }); - const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" }); - const listMethod = useWatch({ control: formControl.control, name: "listMethod" }); - const listType = useWatch({ control: formControl.control, name: "listType" }); - - const isListMethodBlock = listMethod?.value === "Block"; - const isListTypeFileHash = listType?.value === "FileHash"; - const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value); - const isNoExpirationCompatible = isListMethodBlock || - (listMethod?.value === "Allow" && (listType?.value === "Url" || listType?.value === "IP")); - - useEffect(() => { - if (noExpiration) { - formControl.setValue("RemoveAfter", false); - } - - if (removeAfter) { - formControl.setValue("NoExpiration", false); - } - - if (isListMethodBlock) { - formControl.setValue("RemoveAfter", false); - } - - if (listType && !isListTypeSenderUrlOrFileHash) { - formControl.setValue("RemoveAfter", false); - } - - if (isListTypeFileHash) { - formControl.setValue("listMethod", { label: "Block", value: "Block" }); - } - - if (listMethod || listType) { - if (!isNoExpirationCompatible && noExpiration) { - formControl.setValue("NoExpiration", false); - } - } - }, [ - noExpiration, - removeAfter, - isListMethodBlock, - listType, - isListTypeSenderUrlOrFileHash, - isListTypeFileHash, - isNoExpirationCompatible, - formControl - ]); - - const validateEntries = (value) => { - if (!value) return true; - - const entries = value.split(/[,;]/).map(e => e.trim()); - const currentListType = listType?.value; - - if (currentListType === "FileHash") { - for (const entry of entries) { - if (entry.length !== 64) - return "File hash entries must be exactly 64 characters"; - - const hashResult = getCippValidator(entry, "sha256"); - if (hashResult !== true) - return hashResult; - } - } else if (currentListType === "IP") { - for (const entry of entries) { - const ipv6Result = getCippValidator(entry, "ipv6"); - const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); - - if (ipv6Result !== true && ipv6CidrResult !== true) - return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; - } - } else if (currentListType === "Url") { - for (const entry of entries) { - if (entry.length > 250) - return "URL entries must be 250 characters or less"; - - // For entries with wildcards, use the improved wildcard validators - if (entry.includes('*') || entry.includes('~')) { - // Try both wildcard validators - const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); - const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); - - if (wildcardUrlResult !== true && wildcardDomainResult !== true) { - // If basic pattern check fails too, give a more specific message - if (!/^[a-zA-Z0-9\.\-\*\~\/]+$/.test(entry)) { - return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)"; - } - - // If it has basic valid characters but doesn't match our patterns - return "Invalid wildcard format. Common formats are *.domain.com or domain.*"; - } - continue; - } - - // For non-wildcard entries, use standard validators - const ipv4Result = getCippValidator(entry, "ip"); - const ipv4CidrResult = getCippValidator(entry, "ipv4cidr"); - const ipv6Result = getCippValidator(entry, "ipv6"); - const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); - const hostnameResult = getCippValidator(entry, "hostname"); - const urlResult = getCippValidator(entry, "url"); - - // If none of the validators pass - if (ipv4Result !== true && - ipv4CidrResult !== true && - ipv6Result !== true && - ipv6CidrResult !== true && - hostnameResult !== true && - urlResult !== true) { - return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; - } - } - } else if (currentListType === "Sender") { - for (const entry of entries) { - // Check for wildcards first - if (entry.includes('*') || entry.includes('~')) { - const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); - - if (wildcardDomainResult !== true) { - return "Invalid sender wildcard pattern. Common format is *.domain.com"; - } - continue; - } - - // For non-wildcard entries, use senderEntry validator - const senderResult = getCippValidator(entry, "senderEntry"); - - if (senderResult !== true) { - return senderResult; - } - } - } - - return true; - }; - - return ( - { - return { - tenantID: tenantDomain, - entries: values.entries, - listType: values.listType?.value, - notes: values.notes, - listMethod: values.listMethod?.value, - NoExpiration: values.NoExpiration, - RemoveAfter: values.RemoveAfter - }; - }} - > - - {/* Entries */} - - - - {/* Notes & List Type */} - - - - - - - - {/* List Method */} - - - - - {/* No Expiration */} - - - - - {/* Remove After */} - - - - - - ); -}; - -AddTenantAllowBlockList.getLayout = (page) => {page}; - -export default AddTenantAllowBlockList; diff --git a/src/pages/email/administration/tenant-allow-block-lists/index.js b/src/pages/email/administration/tenant-allow-block-lists/index.js index 4770163895fa..628eacd1be83 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/index.js +++ b/src/pages/email/administration/tenant-allow-block-lists/index.js @@ -1,12 +1,11 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button } from "@mui/material"; -import Link from "next/link"; -import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; -import { PlaylistAdd } from "@mui/icons-material"; +import { Delete } from "@mui/icons-material"; +import { CippAddTenantAllowBlockListDrawer } from "/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx"; const Page = () => { const pageTitle = "Tenant Allow/Block Lists"; + const cardButtonPermissions = ["Exchange.SpamFilter.ReadWrite"]; const actions = [ { @@ -19,7 +18,7 @@ const Page = () => { }, confirmText: "Are you sure you want to delete this entry?", color: "danger", - icon: , + icon: , }, ]; @@ -39,21 +38,8 @@ const Page = () => { apiUrl="/api/ListTenantAllowBlockList" actions={actions} simpleColumns={simpleColumns} - titleButton={{ - label: "Add", - href: "/email/administration/tenant-allow-block-list/add", - }} - cardButton={ - <> - - - } + apiDataKey="Results" + cardButton={} /> ); }; diff --git a/src/pages/email/transport/new-rules/add.jsx b/src/pages/email/transport/new-rules/add.jsx index 76083d0ca3d7..4e6029efc601 100644 --- a/src/pages/email/transport/new-rules/add.jsx +++ b/src/pages/email/transport/new-rules/add.jsx @@ -31,6 +31,107 @@ const AddTransportRule = () => { const exceptionTypeWatch = useWatch({ control: formControl.control, name: "exceptionType" }); const applyToAllMessagesWatch = useWatch({ control: formControl.control, name: "applyToAllMessages" }); + // Helper function to get field names for a condition + const getConditionFieldNames = (conditionValue) => { + const fields = [conditionValue]; + // Add related fields for special cases + if (conditionValue === "HeaderContainsWords") { + fields.push("HeaderContainsWordsMessageHeader"); + } else if (conditionValue === "HeaderMatchesPatterns") { + fields.push("HeaderMatchesPatternsMessageHeader"); + } + return fields; + }; + + // Helper function to get field names for an action + const getActionFieldNames = (actionValue) => { + const fields = []; + switch (actionValue) { + case "RejectMessage": + fields.push("RejectMessageReasonText", "RejectMessageEnhancedStatusCode"); + break; + case "SetHeader": + fields.push("SetHeaderName", "SetHeaderValue"); + break; + case "ApplyHtmlDisclaimer": + fields.push("ApplyHtmlDisclaimerText", "ApplyHtmlDisclaimerLocation", "ApplyHtmlDisclaimerFallbackAction"); + break; + default: + fields.push(actionValue); + } + return fields; + }; + + // Update selected conditions and clean up removed ones + useEffect(() => { + const newConditions = conditionTypeWatch || []; + const newConditionValues = newConditions.map(c => c.value || c); + const oldConditionValues = selectedConditions.map(c => c.value || c); + + // Find removed conditions + const removedConditions = oldConditionValues.filter( + oldVal => !newConditionValues.includes(oldVal) + ); + + // Clear form values for removed conditions + removedConditions.forEach(conditionValue => { + const fieldNames = getConditionFieldNames(conditionValue); + fieldNames.forEach(fieldName => { + formControl.setValue(fieldName, undefined); + }); + }); + + setSelectedConditions(newConditions); + }, [conditionTypeWatch]); + + // Update selected actions and clean up removed ones + useEffect(() => { + const newActions = actionTypeWatch || []; + const newActionValues = newActions.map(a => a.value || a); + const oldActionValues = selectedActions.map(a => a.value || a); + + // Find removed actions + const removedActions = oldActionValues.filter( + oldVal => !newActionValues.includes(oldVal) + ); + + // Clear form values for removed actions + removedActions.forEach(actionValue => { + const fieldNames = getActionFieldNames(actionValue); + fieldNames.forEach(fieldName => { + formControl.setValue(fieldName, undefined); + }); + }); + + setSelectedActions(newActions); + }, [actionTypeWatch]); + + // Update selected exceptions and clean up removed ones + useEffect(() => { + const newExceptions = exceptionTypeWatch || []; + const newExceptionValues = newExceptions.map(e => e.value || e); + const oldExceptionValues = selectedExceptions.map(e => e.value || e); + + // Find removed exceptions + const removedExceptions = oldExceptionValues.filter( + oldVal => !newExceptionValues.includes(oldVal) + ); + + // Clear form values for removed exceptions + removedExceptions.forEach(exceptionValue => { + // Get base condition name (remove ExceptIf prefix) + const baseCondition = exceptionValue.replace("ExceptIf", ""); + const fieldNames = getConditionFieldNames(baseCondition).map( + field => field.includes("MessageHeader") ? `ExceptIf${field}` : exceptionValue + ); + fieldNames.forEach(fieldName => { + formControl.setValue(fieldName, undefined); + }); + }); + + setSelectedExceptions(newExceptions); + }, [exceptionTypeWatch]); + // Update selected conditions when conditionType changes useEffect(() => { setSelectedConditions(conditionTypeWatch || []); diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 8c2cf07cbcfa..2afc598b490a 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -123,7 +123,7 @@ const Page = () => { confirmText: "Are you sure you want to locate [deviceName]?", }, { - label: "Retrieve LAPs password", + label: "Retrieve LAPS password", type: "POST", icon: , url: "/api/ExecGetLocalAdminPassword", @@ -131,7 +131,7 @@ const Page = () => { GUID: "azureADDeviceId", }, condition: (row) => row.operatingSystem === "Windows", - confirmText: "Are you sure you want to retrieve the local admin password?", + confirmText: "Are you sure you want to retrieve the local admin password for [deviceName]?", }, { label: "Rotate Local Admin Password", @@ -154,7 +154,19 @@ const Page = () => { GUID: "azureADDeviceId", }, condition: (row) => row.operatingSystem === "Windows", - confirmText: "Are you sure you want to retrieve the BitLocker keys?", + confirmText: "Are you sure you want to retrieve the BitLocker keys for [deviceName]?", + }, + { + label: "Retrieve File Vault Key", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "getFileVaultKey", + }, + condition: (row) => row.operatingSystem === "macOS", + confirmText: "Are you sure you want to retrieve the file vault key for [deviceName]?", }, { label: "Windows Defender Full Scan", diff --git a/src/pages/endpoint/MEM/list-templates/edit.jsx b/src/pages/endpoint/MEM/list-templates/edit.jsx index 71eb1bc6c00c..c58cbe7e0920 100644 --- a/src/pages/endpoint/MEM/list-templates/edit.jsx +++ b/src/pages/endpoint/MEM/list-templates/edit.jsx @@ -19,7 +19,7 @@ const EditIntuneTemplate = () => { }); const templateData = Array.isArray(templateQuery.data) - ? templateQuery.data.find((t) => t.id === id) + ? templateQuery.data.find((t) => t.id === id || t.GUID === id) : templateQuery.data; // Custom data formatter to convert autoComplete objects to values diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index 2c06faf5f4da..d35cf8dc097e 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -139,13 +139,14 @@ const Page = () => { size: "lg", }; - const simpleColumns = ["displayName", "package", "description", "Type"]; + const simpleColumns = ["displayName", "isSynced", "package", "description", "Type"]; return ( <> { RemoveOwner: [], AddContact: [], RemoveContact: [], + visibility: "Public", }, }); @@ -74,6 +75,7 @@ const EditGroup = () => { allowExternal: groupInfo?.data?.allowExternal, sendCopies: groupInfo?.data?.sendCopies, hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, + visibility: group?.visibility ?? "Public", displayName: group.displayName, description: group.description || "", membershipRules: group.membershipRule || "", @@ -114,6 +116,7 @@ const EditGroup = () => { sendCopies: groupInfo?.data?.sendCopies, hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, securityEnabled: group.securityEnabled, + visibility: group.visibility ?? "Public", }); // Reset the form with all values @@ -132,6 +135,7 @@ const EditGroup = () => { "sendCopies", "hideFromOutlookClients", "securityEnabled", + "visibility", ]; changeDetectionProperties.forEach((property) => { @@ -377,6 +381,24 @@ const EditGroup = () => { Group Settings + + {groupType === "Microsoft 365" && ( + + + + )} + {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( { } title="JIT Admin Table" - apiUrl="/api/ExecJITAdmin?Action=List" + apiUrl="/api/ListJITAdmin" apiDataKey="Results" simpleColumns={[]} /> diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index 8baf9eaaf76b..27148701979b 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -6,6 +6,8 @@ import { useCippUserActions } from "/src/components/CippComponents/CippUserActio import { CippInviteGuestDrawer } from "/src/components/CippComponents/CippInviteGuestDrawer.jsx"; import { CippBulkUserDrawer } from "/src/components/CippComponents/CippBulkUserDrawer.jsx"; import { CippAddUserDrawer } from "/src/components/CippComponents/CippAddUserDrawer.jsx"; +import { CippApiLogsDrawer } from "/src/components/CippComponents/CippApiLogsDrawer.jsx"; +import { Box } from "@mui/material"; const Page = () => { const userActions = useCippUserActions(); @@ -57,7 +59,7 @@ const Page = () => { title={pageTitle} apiUrl="/api/ListGraphRequest" cardButton={ - <> + { requiredPermissions={cardButtonPermissions} PermissionButton={PermissionButton} /> - + + } apiData={{ Endpoint: "users", diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index b81765ed651d..eb51eeaff7f1 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -128,6 +128,16 @@ const Page = () => { }; } + // Handle arrays by joining them + if (Array.isArray(userIdentifier)) { + userIdentifier = userIdentifier.join(", "); + } + + // Ensure userIdentifier is a string + if (typeof userIdentifier !== "string") { + userIdentifier = String(userIdentifier); + } + // Handle special built-in cases if (userIdentifier === "Default" || userIdentifier === "Anonymous") { return { @@ -147,7 +157,7 @@ const Page = () => { // Exact match on display name (group.displayName && group.displayName === userIdentifier) || // Partial match - permission identifier starts with group display name (handles timestamps) - (group.displayName && userIdentifier?.startsWith(group.displayName)) + (group.displayName && userIdentifier.startsWith(group.displayName)) ); }); @@ -316,10 +326,15 @@ const Page = () => { useEffect(() => { if (userRequest.isSuccess && userRequest.data?.[0]) { const currentSettings = userRequest.data[0]; - const forwardingAddress = currentSettings.ForwardingAddress; + let forwardingAddress = currentSettings.ForwardingAddress; const forwardingSmtpAddress = currentSettings.MailboxActionsData?.ForwardingSmtpAddress; const forwardAndDeliver = currentSettings.ForwardAndDeliver; + // Handle ForwardingAddress being an array or string + if (Array.isArray(forwardingAddress)) { + forwardingAddress = forwardingAddress.join(", "); + } + let forwardingType = "disabled"; let cleanAddress = ""; @@ -1245,7 +1260,7 @@ const Page = () => { isCollapsible={true} /> diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index cb51e6235fbc..b5a1b86032dd 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -148,6 +148,7 @@ const AlertWizard = () => { recurrence: recurrenceOption, postExecution: postExecutionValue, startDateTime: startDateTimeForForm, + AlertComment: alert.RawAlert.AlertComment || "", }; // Parse Parameters field if it exists and is a string @@ -211,6 +212,7 @@ const AlertWizard = () => { Actions: alert.RawAlert.Actions, conditions: formattedConditions, logbook: foundLogbook, + AlertComment: alert.RawAlert.AlertComment || "", }; formControl.reset(resetData); @@ -344,6 +346,7 @@ const AlertWizard = () => { DesiredStartTime: values.startDateTime ? values.startDateTime.toString() : null, Recurrence: values.recurrence, PostExecution: values.postExecution, + AlertComment: values.AlertComment, }; apiRequest.mutate({ url: "/api/AddScheduledItem?hidden=true", data: postObject }); }; @@ -623,6 +626,17 @@ const AlertWizard = () => { options={actionsToTake} /> + + + @@ -757,6 +771,17 @@ const AlertWizard = () => { options={postExecutionOptions} /> + + + diff --git a/src/pages/tenant/administration/alert-configuration/index.js b/src/pages/tenant/administration/alert-configuration/index.js index 8040f018d37f..2b2864d14272 100644 --- a/src/pages/tenant/administration/alert-configuration/index.js +++ b/src/pages/tenant/administration/alert-configuration/index.js @@ -1,76 +1,77 @@ -import { Button } from "@mui/material"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. -import Link from "next/link"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; - -const Page = () => { - const pageTitle = "Alerts"; - const actions = [ - { - label: "View Task Details", - link: "/cipp/scheduler/task?id=[RowKey]", - icon: , - condition: (row) => row?.EventType === "Scheduled Task", - }, - { - label: "Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", - icon: , - color: "success", - target: "_self", - }, - { - label: "Clone & Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", - icon: , - color: "success", - target: "_self", - }, - { - label: "Delete Alert", - type: "POST", - url: "/api/RemoveQueuedAlert", - data: { - ID: "RowKey", - EventType: "EventType", - }, - icon: , - relatedQueryKeys: "ListAlertsQueue", - confirmText: "Are you sure you want to delete this Alert?", - multiPost: false, - }, - ]; - - return ( - } - > - Add Alert - - } - actions={actions} - simpleColumns={[ - "Tenants", - "EventType", - "Conditions", - "RepeatsEvery", - "Actions", - "excludedTenants", - ]} - queryKey="ListAlertsQueue" - /> - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import Link from "next/link"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Alerts"; + const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + condition: (row) => row?.EventType === "Scheduled Task", + }, + { + label: "Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Clone & Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", + icon: , + color: "success", + target: "_self", + }, + { + label: "Delete Alert", + type: "POST", + url: "/api/RemoveQueuedAlert", + data: { + ID: "RowKey", + EventType: "EventType", + }, + icon: , + relatedQueryKeys: "ListAlertsQueue", + confirmText: "Are you sure you want to delete this Alert?", + multiPost: false, + }, + ]; + + return ( + } + > + Add Alert + + } + actions={actions} + simpleColumns={[ + "Tenants", + "EventType", + "Conditions", + "RepeatsEvery", + "Actions", + "AlertComment", + "excludedTenants", + ]} + queryKey="ListAlertsQueue" + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/tenants/groups/edit.js b/src/pages/tenant/administration/tenants/groups/edit.js index 40326136d39e..fa52b69d553b 100644 --- a/src/pages/tenant/administration/tenants/groups/edit.js +++ b/src/pages/tenant/administration/tenants/groups/edit.js @@ -1,12 +1,10 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; -import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "/src/api/ApiCall"; import { useEffect } from "react"; import { useRouter } from "next/router"; import { Box } from "@mui/material"; -import { Grid } from "@mui/system"; -import CippPageCard from "/src/components/CippCards/CippPageCard"; -import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippAddEditTenantGroups from "/src/components/CippComponents/CippAddEditTenantGroups"; const Page = () => { @@ -21,53 +19,156 @@ const Page = () => { queryKey: id ? `TenantGroupProperties_${id}` : null, }); - const updateGroupApi = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: [ - `TenantGroupProperties_${id}`, - "TenantGroupListPage", - ], - }); - useEffect(() => { if (groupDetails.isSuccess && groupDetails.data) { + const groupData = groupDetails?.data?.Results?.[0]; + + // Determine if this is a dynamic or static group + const isDynamic = groupData?.GroupType === "dynamic" && groupData?.DynamicRules; + + // Format dynamic rules if they exist + let formattedDynamicRules = [{}]; + if (isDynamic && groupData.DynamicRules) { + try { + let rules; + if (Array.isArray(groupData.DynamicRules)) { + rules = groupData.DynamicRules; + } else if (typeof groupData.DynamicRules === "string") { + rules = JSON.parse(groupData.DynamicRules); + } else if (typeof groupData.DynamicRules === "object") { + rules = [groupData.DynamicRules]; + } else { + rules = []; + } + + formattedDynamicRules = rules.map((rule) => { + // Handle value - it's always an array of objects from the backend + let valueForForm; + if (Array.isArray(rule.value)) { + // If it's an array of objects, extract all values + valueForForm = rule.value.map((item) => ({ + label: item.label || item.value || item, + value: item.value || item, + })); + // For single selection operators, take just the first item + if (rule.operator === "eq" || rule.operator === "ne") { + valueForForm = valueForForm[0]; + } + } else if (typeof rule.value === "object" && rule.value?.value) { + // If it's a single object with a value property + valueForForm = { + label: rule.value.label || rule.value.value, + value: rule.value.value, + }; + } else { + // Simple value + valueForForm = { + label: rule.value, + value: rule.value, + }; + } + + return { + property: { + label: + rule.property === "availableLicense" + ? "Available License" + : rule.property === "availableServicePlan" + ? "Available Service Plan" + : rule.property === "delegatedAccessStatus" + ? "Delegated Access Status" + : rule.property, + value: rule.property, + type: + rule.property === "availableLicense" + ? "license" + : rule.property === "availableServicePlan" + ? "servicePlan" + : rule.property === "delegatedAccessStatus" + ? "delegatedAccess" + : "unknown", + }, + operator: { + label: + rule.operator === "eq" + ? "Equals" + : rule.operator === "ne" + ? "Not Equals" + : rule.operator === "in" + ? "In" + : rule.operator === "notIn" + ? "Not In" + : rule.operator, + value: rule.operator, + }, + value: valueForForm, + }; + }); + } catch (e) { + console.error("Error parsing dynamic rules:", e, groupData.DynamicRules); + formattedDynamicRules = [{}]; + } + } + formControl.reset({ groupId: id, - groupName: groupDetails?.data?.Results?.[0]?.Name ?? "", - groupDescription: groupDetails?.data?.Results?.[0]?.Description ?? "", - members: - groupDetails?.data?.Results?.[0]?.Members?.map((member) => ({ - label: member.displayName, - value: member.customerId, - })) || [], + groupName: groupData?.Name ?? "", + groupDescription: groupData?.Description ?? "", + groupType: isDynamic ? "dynamic" : "static", + ruleLogic: groupData?.RuleLogic || "and", + members: !isDynamic + ? groupData?.Members?.map((member) => ({ + label: member.displayName, + value: member.customerId, + })) || [] + : [], + dynamicRules: formattedDynamicRules, }); } }, [groupDetails.isSuccess, groupDetails.data]); + const customDataFormatter = (values) => { + const formattedData = { + ...values, + Action: "AddEdit", + }; + + // If it's a dynamic group, format the rules for the backend + if (values.groupType === "dynamic" && values.dynamicRules) { + formattedData.dynamicRules = values.dynamicRules.map((rule) => ({ + property: rule.property?.value || rule.property, + operator: rule.operator?.value || rule.operator, + value: rule.value, + })); + formattedData.ruleLogic = values.ruleLogic || "and"; + } + + return formattedData; + }; + return ( - - - - - - + - - - + ); }; diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js index 8d3e4c328ceb..ea0416d936bf 100644 --- a/src/pages/tenant/administration/tenants/groups/index.js +++ b/src/pages/tenant/administration/tenants/groups/index.js @@ -2,22 +2,43 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import tabOptions from "../tabOptions"; -import { Edit } from "@mui/icons-material"; -import { Button, SvgIcon } from "@mui/material"; -import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; -import NextLink from "next/link"; +import { Edit, PlayArrow, GroupAdd } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippAddTenantGroupDrawer } from "/src/components/CippComponents/CippAddTenantGroupDrawer"; +import { CippApiLogsDrawer } from "/src/components/CippComponents/CippApiLogsDrawer"; +import { CippTenantGroupOffCanvas } from "/src/components/CippComponents/CippTenantGroupOffCanvas"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; +import { Box, Button } from "@mui/material"; +import { useDialog } from "/src/hooks/use-dialog.js"; const Page = () => { const pageTitle = "Tenant Groups"; + const createDefaultGroupsDialog = useDialog(); - const simpleColumns = ["Name", "Description", "Members"]; + const simpleColumns = ["Name", "Description", "GroupType", "Members"]; + const offcanvas = { + children: (row) => { + return ; + }, + size: "xl", + }; const actions = [ { label: "Edit Group", link: "/tenant/administration/tenants/groups/edit?id=[Id]", icon: , }, + { + label: "Run Dynamic Rules", + icon: , + url: "/api/ExecRunTenantGroupRule", + type: "POST", + data: { groupId: "Id" }, + queryKey: "TenantGroupListPage", + confirmText: "Are you sure you want to run dynamic rules for [Name]?", + condition: (row) => row.GroupType === "dynamic", + }, { label: "Delete Group", icon: , @@ -26,35 +47,47 @@ const Page = () => { data: { action: "Delete", groupId: "Id" }, queryKey: "TenantGroupListPage", confirmText: "Are you sure you want to delete [Name]?", - } + }, ]; return ( - - - - } - > - Add Tenant Group - - } - /> + <> + + + + + + } + offCanvas={offcanvas} + /> + + ); }; diff --git a/src/pages/tenant/conditional/list-named-locations/index.js b/src/pages/tenant/conditional/list-named-locations/index.js index 90bb4e3d43a2..8b19e50281cc 100644 --- a/src/pages/tenant/conditional/list-named-locations/index.js +++ b/src/pages/tenant/conditional/list-named-locations/index.js @@ -11,6 +11,7 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; import { LocationOn } from "@mui/icons-material"; +import countryList from "/src/data/countryList.json"; const Page = () => { const pageTitle = "Named Locations"; @@ -62,8 +63,18 @@ const Page = () => { namedLocationId: "id", change: "!addLocation", }, - fields: [{ type: "textField", name: "input", label: "Country Code" }], - confirmText: "Enter a two-letter country code, e.g., US.", + fields: [ + { + type: "autoComplete", + name: "input", + label: "Country", + options: countryList.map(({ Code, Name }) => ({ + value: Code, + label: `${Name} (${Code})`, + })), + }, + ], + confirmText: "Select a country to add to this named location.", condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { @@ -75,8 +86,18 @@ const Page = () => { namedLocationId: "id", change: "!removeLocation", }, - fields: [{ type: "textField", name: "input", label: "Country Code" }], - confirmText: "Enter a two-letter country code, e.g., US.", + fields: [ + { + type: "autoComplete", + name: "input", + label: "Country", + options: countryList.map(({ Code, Name }) => ({ + value: Code, + label: `${Name} (${Code})`, + })), + }, + ], + confirmText: "Select a country to remove from this named location.", condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { diff --git a/src/pages/tenant/gdap-management/invites/add.js b/src/pages/tenant/gdap-management/invites/add.js index 22a45e334d1d..ce6fe95f1226 100644 --- a/src/pages/tenant/gdap-management/invites/add.js +++ b/src/pages/tenant/gdap-management/invites/add.js @@ -118,7 +118,7 @@ const Page = () => { The onboarding process will also run on a nightly schedule. For automated onboardings, please check out{" "} - Partner Webhooks + Automated Onboarding {" "} in Application Settings. diff --git a/src/pages/tenant/manage/edit.js b/src/pages/tenant/manage/edit.js index 8ab89052ca89..d5ddc69085c2 100644 --- a/src/pages/tenant/manage/edit.js +++ b/src/pages/tenant/manage/edit.js @@ -174,7 +174,6 @@ const Page = () => { { + const country = countryList.find((c) => c.Code === countryCode); + return country ? country.Name : countryCode; +}; export const getCippFormatting = (data, cellName, type, canReceive, flatten = true) => { const isText = type === "text"; @@ -415,6 +423,19 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : ; } + if (cellName === "countriesAndRegions") { + if (Array.isArray(data)) { + const countryNames = data + .filter((item) => item !== null && item !== undefined) + .map((countryCode) => getCountryNameFromCode(countryCode)); + + return isText ? countryNames.join(", ") : renderChipList(countryNames); + } else { + const countryName = getCountryNameFromCode(data); + return isText ? countryName : ; + } + } + if (cellName === "excludedTenants") { // Handle null or undefined data if (data === null || data === undefined) { @@ -467,13 +488,56 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr } if (cellName === "state") { - data = - data === "enabled" + if (typeof data !== "string") { + return isText ? data : ; + } + + const normalized = data.trim().toLowerCase(); + const label = + normalized === "enabled" ? "Enabled" - : data === "enabledForReportingButNotEnforced" + : normalized === "disabled" + ? "Disabled" + : normalized === "enabledforreportingbutnotenforced" || + normalized === "report-only" || + normalized === "reportonly" ? "Report Only" - : data; - return isText ? data : ; + : data.charAt(0).toUpperCase() + data.slice(1); + + if (isText) { + return label; + } + + const chipProps = { + size: "small", + label, + variant: "filled", + color: "info", + }; + + if (normalized === "enabled") { + chipProps.color = "info"; + } else if (normalized === "disabled") { + chipProps.color = "default"; + chipProps.sx = (theme) => ({ + bgcolor: + theme.palette.mode === "dark" + ? alpha(theme.palette.common.white, 0.12) + : alpha(theme.palette.text.primary, 0.08), + color: theme.palette.text.primary, + borderColor: "transparent", + }); + } else if ( + normalized === "enabledforreportingbutnotenforced" || + normalized === "report-only" || + normalized === "reportonly" + ) { + chipProps.color = "warning"; + } else { + chipProps.variant = "outlined"; + } + + return ; } if (cellName === "Parameters.ScheduledBackupValues") { diff --git a/src/utils/get-cipp-tenant-group-options.js b/src/utils/get-cipp-tenant-group-options.js new file mode 100644 index 000000000000..7a27e526c16c --- /dev/null +++ b/src/utils/get-cipp-tenant-group-options.js @@ -0,0 +1,183 @@ +import M365LicensesDefault from "../data/M365Licenses.json"; +import M365LicensesAdditional from "../data/M365Licenses-additional.json"; + +/** + * Get all available licenses for tenant group dynamic rules + * @returns {Array} Array of license options with label and value (SKU) + */ +export const getTenantGroupLicenseOptions = () => { + // Combine both license files + const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional]; + + // Create unique licenses map using String_Id as key for better deduplication + const uniqueLicensesMap = new Map(); + + allLicenses.forEach((license) => { + if (license.String_Id && license.Product_Display_Name && license.GUID) { + // Use String_Id as the unique key since that's what we send to backend + const key = license.String_Id; + if (!uniqueLicensesMap.has(key)) { + uniqueLicensesMap.set(key, { + label: license.Product_Display_Name, + value: license.String_Id, + guid: license.GUID, + }); + } + } + }); + + // Convert to array and filter out incomplete entries + const licenseOptions = Array.from(uniqueLicensesMap.values()).filter( + (license) => license.label && license.value + ); + + // Additional deduplication by label to handle cases where different String_Ids have same display name + const uniqueByLabelMap = new Map(); + licenseOptions.forEach((license) => { + if (!uniqueByLabelMap.has(license.label)) { + uniqueByLabelMap.set(license.label, license); + } + }); + + return Array.from(uniqueByLabelMap.values()).sort((a, b) => a.label.localeCompare(b.label)); +}; + +/** + * Get all available service plans for tenant group dynamic rules + * @returns {Array} Array of unique service plan options with label and value + */ +export const getTenantGroupServicePlanOptions = () => { + // Combine both license files + const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional]; + + // Create unique service plans map using Service_Plan_Name as key for better deduplication + const uniqueServicePlansMap = new Map(); + + allLicenses.forEach((license) => { + if ( + license.Service_Plan_Name && + license.Service_Plans_Included_Friendly_Names && + license.Service_Plan_Id + ) { + // Use Service_Plan_Name as the unique key since that's what we send to backend + const key = license.Service_Plan_Name; + if (!uniqueServicePlansMap.has(key)) { + uniqueServicePlansMap.set(key, { + label: license.Service_Plans_Included_Friendly_Names, + value: license.Service_Plan_Name, + id: license.Service_Plan_Id, + }); + } + } + }); + + // Convert to array and sort by display name, then deduplicate by label as well + const serviceOptions = Array.from(uniqueServicePlansMap.values()).filter( + (plan) => plan.label && plan.value + ); // Filter out any incomplete entries + + // Additional deduplication by label to handle cases where different service plan names have same friendly name + const uniqueByLabelMap = new Map(); + serviceOptions.forEach((plan) => { + if (!uniqueByLabelMap.has(plan.label)) { + uniqueByLabelMap.set(plan.label, plan); + } + }); + + return Array.from(uniqueByLabelMap.values()).sort((a, b) => a.label.localeCompare(b.label)); +}; + +/** + * Get delegated access status options for tenant group dynamic rules + * @returns {Array} Array of delegated access status options + */ +export const getTenantGroupDelegatedAccessOptions = () => { + return [ + { + label: "Granular Delegated Admin Privileges", + value: "granularDelegatedAdminPrivileges", + }, + { + label: "Direct Tenant", + value: "directTenant", + }, + ]; +}; + +/** + * Get all property options for dynamic tenant group rules + * @returns {Array} Array of property options for the rule builder + */ +export const getTenantGroupPropertyOptions = () => { + return [ + { + label: "Available License", + value: "availableLicense", + type: "license", + }, + { + label: "Available Service Plan", + value: "availableServicePlan", + type: "servicePlan", + }, + { + label: "Delegated Access Status", + value: "delegatedAccessStatus", + type: "delegatedAccess", + }, + ]; +}; + +/** + * Get operator options for dynamic tenant group rules + * @returns {Array} Array of operator options + */ +export const getTenantGroupOperatorOptions = (propertyType) => { + const baseOperators = [ + { + label: "Equals", + value: "eq", + }, + { + label: "Not Equals", + value: "ne", + } + ]; + + const arrayOperators = [ + { + label: "In", + value: "in", + }, + { + label: "Not In", + value: "notIn", + } + ]; + + // Delegated Access Status only supports equals/not equals + if (propertyType === "delegatedAccess") { + return baseOperators; + } + + // License and Service Plan support all operators + return [...baseOperators, ...arrayOperators]; +}; + +/** + * Get value options based on the selected property type + * @param {string} propertyType - The type of property (license, servicePlan, delegatedAccess) + * @returns {Array} Array of value options for the selected property type + */ +export const getTenantGroupValueOptions = (propertyType) => { + switch (propertyType) { + case "license": + return getTenantGroupLicenseOptions(); + case "servicePlan": + return getTenantGroupServicePlanOptions(); + case "delegatedAccess": + return getTenantGroupDelegatedAccessOptions(); + default: + return []; + } +}; diff --git a/yarn.lock b/yarn.lock index 860b5de8865c..bdbd2027fafb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2225,6 +2225,13 @@ "@types/linkify-it" "^5" "@types/mdurl" "^2" +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== + dependencies: + "@types/unist" "^2" + "@types/mdast@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" @@ -3299,6 +3306,11 @@ dfa@^1.2.0: resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== +diff@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3446,6 +3458,11 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3588,6 +3605,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + eslint-config-next@15.5.2: version "15.5.2" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.2.tgz#9629ed1deaa131e8e80cbae20acf631c8595ca3e" @@ -4183,11 +4205,51 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + hast-util-parse-selector@^2.0.0: version "2.2.5" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e" + integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + hast-util-to-jsx-runtime@^2.0.0: version "2.3.6" resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" @@ -4209,6 +4271,19 @@ hast-util-to-jsx-runtime@^2.0.0: unist-util-position "^5.0.0" vfile-message "^4.0.0" +hast-util-to-parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + hast-util-whitespace@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" @@ -4227,6 +4302,17 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + highlight-words@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936" @@ -4284,6 +4370,11 @@ html-url-attributes@^3.0.0: resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87" integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + html2canvas@^1.0.0-rc.5: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -4447,6 +4538,11 @@ is-boolean-object@^1.2.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-bun-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" @@ -4790,6 +4886,11 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kleur@^4.0.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -4928,6 +5029,11 @@ markdown-it@^14.0.0: punycode.js "^2.3.1" uc.micro "^2.1.0" +markdown-table@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== + material-react-table@^3.0.1: version "3.2.1" resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.2.1.tgz#56f595755cab3b669b399999fed9eb305fbb6dd7" @@ -4943,6 +5049,34 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + mdast-util-from-markdown@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" @@ -4961,6 +5095,64 @@ mdast-util-from-markdown@^2.0.0: micromark-util-types "^2.0.0" unist-util-stringify-position "^4.0.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + mdast-util-mdx-expression@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" @@ -5003,6 +5195,14 @@ mdast-util-mdxjs-esm@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" + integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== + dependencies: + "@types/mdast" "^3.0.0" + unist-util-is "^5.0.0" + mdast-util-phrasing@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" @@ -5026,6 +5226,20 @@ mdast-util-to-hast@^13.0.0: unist-util-visit "^5.0.0" vfile "^6.0.0" +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" + integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + mdast-util-to-markdown@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" @@ -5041,6 +5255,13 @@ mdast-util-to-markdown@^2.0.0: unist-util-visit "^5.0.0" zwitch "^2.0.0" +mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" @@ -5083,6 +5304,28 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -5105,6 +5348,94 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" + integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" + integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" + integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" + integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" + +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" + integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-destination@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" @@ -5114,6 +5445,16 @@ micromark-factory-destination@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-factory-label@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" @@ -5124,6 +5465,14 @@ micromark-factory-label@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-space@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" @@ -5132,6 +5481,16 @@ micromark-factory-space@^2.0.0: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-title@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" @@ -5142,6 +5501,16 @@ micromark-factory-title@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-whitespace@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" @@ -5152,6 +5521,14 @@ micromark-factory-whitespace@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-character@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" @@ -5160,6 +5537,13 @@ micromark-util-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-chunked@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" @@ -5167,6 +5551,15 @@ micromark-util-chunked@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-classify-character@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" @@ -5176,6 +5569,14 @@ micromark-util-classify-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-combine-extensions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" @@ -5184,6 +5585,13 @@ micromark-util-combine-extensions@^2.0.0: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-decode-numeric-character-reference@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" @@ -5191,6 +5599,16 @@ micromark-util-decode-numeric-character-reference@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-decode-string@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" @@ -5201,16 +5619,33 @@ micromark-util-decode-string@^2.0.0: micromark-util-decode-numeric-character-reference "^2.0.0" micromark-util-symbol "^2.0.0" +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + micromark-util-encode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + micromark-util-html-tag-name@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-normalize-identifier@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" @@ -5218,6 +5653,13 @@ micromark-util-normalize-identifier@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + micromark-util-resolve-all@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" @@ -5225,6 +5667,15 @@ micromark-util-resolve-all@^2.0.0: dependencies: micromark-util-types "^2.0.0" +micromark-util-sanitize-uri@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-sanitize-uri@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" @@ -5234,6 +5685,16 @@ micromark-util-sanitize-uri@^2.0.0: micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-util-subtokenize@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" @@ -5244,16 +5705,49 @@ micromark-util-subtokenize@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + micromark-util-symbol@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + micromark-util-types@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromark@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" @@ -5323,6 +5817,11 @@ monaco-editor@^0.53.0: dependencies: "@types/trusted-types" "^1.0.6" +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -5611,6 +6110,13 @@ parse-svg-path@^0.1.2: resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb" integrity sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ== +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -5711,6 +6217,11 @@ property-information@^5.0.0: dependencies: xtend "^4.0.0" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + property-information@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" @@ -6319,11 +6830,30 @@ regjsparser@^0.12.0: dependencies: jsesc "~3.0.2" +rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + relative-time-format@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/relative-time-format/-/relative-time-format-1.1.6.tgz#724a5fbc3794b8e0471b6b61419af2ce699eb9f1" integrity sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ== +remark-gfm@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-parse@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" @@ -6420,6 +6950,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-array-concat@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" @@ -7090,6 +7627,19 @@ unicode-trie@^2.0.0: pako "^0.2.5" tiny-inflate "^1.0.0" +unified@^10.0.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + unified@^11.0.0: version "11.0.5" resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" @@ -7103,6 +7653,13 @@ unified@^11.0.0: trough "^2.0.0" vfile "^6.0.0" +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" @@ -7117,6 +7674,13 @@ unist-util-position@^5.0.0: dependencies: "@types/unist" "^3.0.0" +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" @@ -7124,6 +7688,14 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" +unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" @@ -7132,6 +7704,15 @@ unist-util-visit-parents@^6.0.0: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" +unist-util-visit@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + unist-util-visit@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" @@ -7205,6 +7786,32 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" @@ -7213,6 +7820,16 @@ vfile-message@^4.0.0: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" +vfile@^5.0.0: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + vfile@^6.0.0: version "6.0.3" resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" @@ -7240,6 +7857,11 @@ w3c-keyname@^2.2.0: resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"