diff --git a/src/components/CippCards/CippButtonCard.jsx b/src/components/CippCards/CippButtonCard.jsx index 73f1009e1ec7..ca9429af940b 100644 --- a/src/components/CippCards/CippButtonCard.jsx +++ b/src/components/CippCards/CippButtonCard.jsx @@ -42,8 +42,12 @@ export default function CippButtonCard({ {component === "card" && ( <> - - + {title && ( + <> + + + + )} {isFetching ? : children} diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index 507a26667bbd..bce1d9e7d65d 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -48,13 +48,14 @@ export const CippCodeBlock = (props) => { {type === "editor" && ( diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index b8e5b548e94d..abbe5aa682a4 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -23,6 +23,8 @@ export const CippOffCanvas = (props) => { onNavigateDown, canNavigateUp = false, canNavigateDown = false, + contentPadding = 2, + keepMounted = false, } = props; const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -80,7 +82,7 @@ export const CippOffCanvas = (props) => { sx: { width: drawerWidth }, }} ModalProps={{ - keepMounted: false, + keepMounted: keepMounted, }} anchor={"right"} open={visible} @@ -152,7 +154,13 @@ export const CippOffCanvas = (props) => { sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }} > {/* Render children if provided, otherwise render default content */} {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx b/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx index 6fdf41b42d40..ec96e59f2765 100644 --- a/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx +++ b/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx @@ -1,5 +1,5 @@ import { Grid } from "@mui/system"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormComponent from "../CippComponents/CippFormComponent"; const CippAddIntuneReusableSettingTemplateForm = ({ formControl }) => { return ( diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 32c28c31e84a..a1a14f3ecc40 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1322,10 +1322,22 @@ export const CIPPTableToptoolbar = ({ title="Edit Filters" visible={filterCanvasVisible} onClose={() => setFilterCanvasVisible(!filterCanvasVisible)} + contentPadding={1} + keepMounted={true} > f.filterName === activeFilterName) + : null + } + onPresetSelect={(preset) => { + if (preset?.value && preset?.type === "graph") { + setTableFilter(preset.value, preset.type, preset.filterName); + } + }} onSubmitFilter={(filter) => { setTableFilter(filter, "graph", "Custom Filter"); if (filter?.$select) { diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 9d296e50ef2f..d95654de1594 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { Button, Link, Typography } from "@mui/material"; +import { Box, Button, Link, Typography } from "@mui/material"; import { Save as SaveIcon, Delete, @@ -29,6 +29,9 @@ const CippGraphExplorerFilter = ({ onPresetChange, component = "accordion", relatedQueryKeys = [], + selectedPreset = null, + onPresetSelect, + hideButtons = false, }) => { const [offCanvasOpen, setOffCanvasOpen] = useState(false); const [cardExpanded, setCardExpanded] = useState(true); @@ -123,7 +126,7 @@ const CippGraphExplorerFilter = ({ .filter( (item) => !endpointFilter || - normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter) + normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter), ) .forEach((item) => { presetOptionList.push({ @@ -153,7 +156,7 @@ const CippGraphExplorerFilter = ({ propertyList.refetch(); } }, 1000), - [currentEndpoint] // Dependencies that the debounce function depends on + [currentEndpoint], // Dependencies that the debounce function depends on ); useEffect(() => { @@ -180,14 +183,29 @@ const CippGraphExplorerFilter = ({ }); }; + const deletePreset = (id) => { + savePresetApi.mutate({ + url: "/api/ExecGraphExplorerPreset", + data: { action: "Delete", preset: { id: selectedPresetState } }, + }); + }; + const selectedPresets = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // Sync with parent component's selected preset + useEffect(() => { + if (selectedPreset && selectedPreset.value !== selectedPresets?.value) { + presetControl.setValue("reportTemplate", selectedPreset); + } + }, [selectedPreset?.value]); + useEffect(() => { if (selectedPresets?.addedFields?.params) { setPresetOwner(selectedPresets?.addedFields?.IsMyPreset ?? false); Object.keys(selectedPresets.addedFields.params).forEach( (key) => selectedPresets.addedFields.params[key] == null && - delete selectedPresets.addedFields.params[key] + delete selectedPresets.addedFields.params[key], ); //if $select is a blank array, set it to a string. if ( @@ -233,6 +251,11 @@ const CippGraphExplorerFilter = ({ // save last preset title setLastPresetTitle(selectedPresets.label); formControl.reset(selectedPresets?.addedFields?.params, { keepDefaultValues: true }); + + // Notify parent when preset changes in this component + if (onPresetSelect) { + onPresetSelect(selectedPresets); + } } }, [selectedPresets]); @@ -375,7 +398,7 @@ const CippGraphExplorerFilter = ({ Schedule Graph Explorer Report - + , ); setOffCanvasOpen(true); }; @@ -488,17 +511,10 @@ const CippGraphExplorerFilter = ({ }; //console.log(cardExpanded); - const deletePreset = (id) => { - savePresetApi.mutate({ - url: "/api/ExecGraphExplorerPreset", - data: { action: "Delete", preset: { id: selectedPresetState } }, - }); - }; - return (
setCardExpanded(expanded)} @@ -507,76 +523,8 @@ const CippGraphExplorerFilter = ({ height: "100%", mb: 2, }} - CardButton={ - <> - - - - - - - - - {selectedPresetState && ( - - )} - - - - - - - } > - + - + {/* Reverse Tenant Lookup Switch */} + + {/* Footer-style action section */} + {!hideButtons && ( + + + + {component === "accordion" ? ( + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + + + + )} + + + )} - { + const [offCanvasVisible, setOffCanvasVisible] = useState(false); + const [presetOptions, setPresetOptions] = useState([]); + const [currentFilterValues, setCurrentFilterValues] = useState(null); + + const presetControl = useForm({ + mode: "onChange", + defaultValues: { + reportTemplate: null, + }, + }); + + const selectedPreset = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // API call for available presets + const presetList = ApiGetCall({ + url: "/api/ListGraphExplorerPresets", + queryKey: "ListGraphExplorerPresets", + }); + + useEffect(() => { + var presetOptionList = []; + defaultPresets.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Built-In", + }); + }); + if (presetList.isSuccess && presetList.data?.Results.length > 0) { + presetList.data.Results.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Custom", + }); + }); + } + setPresetOptions(presetOptionList); + }, [defaultPresets, presetList.isSuccess, presetList.data]); + + const handleRunPreset = () => { + if (selectedPreset?.addedFields?.params) { + const params = selectedPreset.addedFields.params; + const values = { ...params }; + + // Handle $select array/string conversion + if (values.$select && Array.isArray(values.$select) && values.$select.length > 0) { + values.$select = values.$select + .map((item) => (typeof item === "string" ? item : item.value)) + .join(","); + } else if (values.$select === "") { + delete values.$select; + } + + // Handle version conversion + if (values.version && values.version.value) { + values.version = values.version.value; + } else if (!values.version) { + values.version = "beta"; + } + + // Clean up false boolean values + if (values.ReverseTenantLookup === false) { + delete values.ReverseTenantLookup; + } + if (values.NoPagination === false) { + delete values.NoPagination; + } + if (values.$count === false) { + delete values.$count; + } + if (values.AsApp === false) { + delete values.AsApp; + } + + // Remove null/empty values + Object.keys(values).forEach((key) => { + if (values[key] === null || values[key] === "") { + delete values[key]; + } + }); + + // Update page title if callback provided + if (onPresetChange && selectedPreset.label) { + onPresetChange(`Graph Explorer - ${selectedPreset.label}`); + } + + setCurrentFilterValues(values); + onSubmitFilter(values); + } + }; + + const handleFilterSubmit = (values) => { + setCurrentFilterValues(values); + onSubmitFilter(values); + setOffCanvasVisible(false); + }; + + const handlePresetChange = (preset) => { + presetControl.setValue("reportTemplate", preset); + }; + + return ( + <> + + + option.type} + renderGroup={(params) => ( +
  • + {params.group} + {params.children} +
  • + )} + placeholder="Select a query to run" + /> +
    + + + + {onViewModeChange && ( + + )} + +
    + + setOffCanvasVisible(false)} + contentPadding={1} + > + + + + ); +}; + +export default CippGraphExplorerSimpleFilter; diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx index c2ef4ec5a281..186389793d25 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx @@ -2,7 +2,7 @@ import { Box } from "@mui/material"; import { useForm } from "react-hook-form"; import { useSettings } from "../../../../hooks/use-settings"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import CippAddIntuneReusableSettingTemplateForm from "../../../../components/CippFormPages/CippAddIntuneReusableSettingTemplateForm"; const Page = () => { diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx index d1ca5e5f444b..40554697f4ec 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx @@ -1,7 +1,19 @@ -import { Alert, Box, Button, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography, Divider } from "@mui/material"; +import { + Alert, + Box, + Button, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, + Divider, +} from "@mui/material"; import { useForm, useFieldArray } from "react-hook-form"; import { useRouter } from "next/router"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; @@ -15,7 +27,10 @@ const deepClone = (obj) => JSON.parse(JSON.stringify(obj)); const generateGuid = () => { const wrap = (val) => `{${val}}`; if (typeof crypto !== "undefined" && crypto.randomUUID) return wrap(crypto.randomUUID()); - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + const s4 = () => + Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); return wrap(`${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`); }; @@ -88,9 +103,13 @@ const EditReusableSettingsTemplate = () => { const groupChildDefinitions = useMemo(() => { const first = groupCollection?.[0]?.children || []; return { - idDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_id"))?.settingDefinitionId, - autoresolveDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_autoresolve"))?.settingDefinitionId, - keywordDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_keyword"))?.settingDefinitionId, + idDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_id")) + ?.settingDefinitionId, + autoresolveDef: first.find((c) => + c.settingDefinitionId?.toLowerCase().includes("_autoresolve"), + )?.settingDefinitionId, + keywordDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_keyword")) + ?.settingDefinitionId, }; }, [groupCollection]); @@ -105,8 +124,14 @@ const EditReusableSettingsTemplate = () => { useEffect(() => { if (normalizedTemplate) { - formControl.setValue("displayName", normalizedTemplate.displayName || normalizedTemplate.name); - formControl.setValue("description", normalizedTemplate.description || normalizedTemplate.Description); + formControl.setValue( + "displayName", + normalizedTemplate.displayName || normalizedTemplate.name, + ); + formControl.setValue( + "description", + normalizedTemplate.description || normalizedTemplate.Description, + ); } }, [normalizedTemplate, formControl]); @@ -185,7 +210,10 @@ const EditReusableSettingsTemplate = () => { processedValues.parsedRAWJson.description = processedValues.description; } - if (processedValues.groupSettingCollectionValue && processedValues.parsedRAWJson.settingInstance) { + if ( + processedValues.groupSettingCollectionValue && + processedValues.parsedRAWJson.settingInstance + ) { processedValues.parsedRAWJson.settingInstance.groupSettingCollectionValue = processedValues.groupSettingCollectionValue; } @@ -356,7 +384,11 @@ const EditReusableSettingsTemplate = () => { - diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/index.js b/src/pages/endpoint/MEM/reusable-settings-templates/index.js index 230f214e3fca..fe6a5810e995 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/index.js +++ b/src/pages/endpoint/MEM/reusable-settings-templates/index.js @@ -1,10 +1,10 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { Button } from "@mui/material"; import Link from "next/link"; import { AddBox, GitHub, Delete, Edit } from "@mui/icons-material"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "../../../../api/ApiCall"; const Page = () => { const pageTitle = "Reusable Settings Templates"; diff --git a/src/pages/endpoint/MEM/reusable-settings/edit.jsx b/src/pages/endpoint/MEM/reusable-settings/edit.jsx index b6dcd1825ffa..3fe089ac520d 100644 --- a/src/pages/endpoint/MEM/reusable-settings/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings/edit.jsx @@ -3,13 +3,13 @@ import { Alert, Box, Stack } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { useRouter } from "next/router"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import CippJsonView from "/src/components/CippFormPages/CippJSONView"; -import { ApiGetCall } from "/src/api/ApiCall"; -import { useSettings } from "/src/hooks/use-settings"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../../hooks/use-settings"; const EditReusableSetting = () => { const router = useRouter(); @@ -72,7 +72,9 @@ const EditReusableSetting = () => { return ( { return ( } + cardButton={ + + } apiUrl="/api/ListIntuneReusableSettings" queryKey={`ListIntuneReusableSettings-${currentTenant}`} actions={actions} diff --git a/src/pages/tenant/tools/graph-explorer/index.js b/src/pages/tenant/tools/graph-explorer/index.js index 2ba20baa9332..3efb93604479 100644 --- a/src/pages/tenant/tools/graph-explorer/index.js +++ b/src/pages/tenant/tools/graph-explorer/index.js @@ -1,22 +1,100 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import CippGraphExplorerFilter from "../../../../components/CippTable/CippGraphExplorerFilter"; +import CippGraphExplorerSimpleFilter from "../../../../components/CippTable/CippGraphExplorerSimpleFilter"; import { useState } from "react"; -import { Grid } from "@mui/system"; +import { Grid, Stack, Box, Container } from "@mui/system"; import { useSettings } from "../../../../hooks/use-settings"; +import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; +import { ApiGetCallWithPagination } from "../../../../api/ApiCall"; +import { CircularProgress, Typography, Card } from "@mui/material"; +import { CippHead } from "../../../../components/CippComponents/CippHead"; const Page = () => { const [apiFilter, setApiFilter] = useState([]); const [pageTitle, setPageTitle] = useState("Graph Explorer"); + const [viewMode, setViewMode] = useState("table"); const tenantFilter = useSettings().currentTenant; const queryKey = JSON.stringify({ apiFilter, tenantFilter }); + const apiData = ApiGetCallWithPagination({ + url: apiFilter.endpoint ? "/api/ListGraphRequest" : "/api/ListEmptyResults", + data: apiFilter, + queryKey: queryKey, + waiting: !!apiFilter.endpoint, + }); + + const jsonData = apiData?.data?.pages?.[0]?.Results || apiData?.data || {}; + + if (viewMode === "json") { + return ( + <> + + + + + + + + + + + {pageTitle} + + + {apiData.isLoading || apiData.isFetching ? ( + + + + Loading data... + + + ) : null} + + + + + + + ); + } + return ( - + }