From 3638845b583ac6dbd7b753c88930cb9108c76cdb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 17:54:41 -0500 Subject: [PATCH 1/9] spacing for api results --- .../administration/users/user/edit.jsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/pages/identity/administration/users/user/edit.jsx b/src/pages/identity/administration/users/user/edit.jsx index 2d793752dfd9..8adfb11635ad 100644 --- a/src/pages/identity/administration/users/user/edit.jsx +++ b/src/pages/identity/administration/users/user/edit.jsx @@ -15,6 +15,7 @@ import tabOptions from "./tabOptions"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; import { Button } from "@mui/material"; +import { Box } from "@mui/system"; const Page = () => { const userSettingsDefaults = useSettings(); const router = useRouter(); @@ -81,15 +82,15 @@ const Page = () => { icon: , text: ( + color="muted" + style={{ paddingLeft: 0 }} + size="small" + href={`https://entra.microsoft.com/${userSettingsDefaults.currentTenant}/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/${userId}`} + target="_blank" + rel="noopener noreferrer" + > + View in Entra + ), }, ] @@ -113,11 +114,13 @@ const Page = () => { > {userRequest.isLoading && } {userRequest.isSuccess && ( - + + + )} From 085f8cc55f616ba9d039f8ba9a127757e8bfa6f7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 18:00:26 -0500 Subject: [PATCH 2/9] disable option creation on tenant selector --- src/components/CippComponents/CippFormTenantSelector.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippComponents/CippFormTenantSelector.jsx b/src/components/CippComponents/CippFormTenantSelector.jsx index 032d4d6ad96f..d4e0b6587c76 100644 --- a/src/components/CippComponents/CippFormTenantSelector.jsx +++ b/src/components/CippComponents/CippFormTenantSelector.jsx @@ -38,6 +38,7 @@ export const CippFormTenantSelector = ({ customerId: "customerId", }, }} + creatable={false} multiple={type === "single" ? false : true} disableClearable={disableClearable} validators={validators} From eb903beb377dcbbb7a88b4204075377f90fa8365 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 18:01:07 -0500 Subject: [PATCH 3/9] Scheduler: default to textField values on --- src/components/CippFormPages/CippSchedulerForm.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index 5bc71dd02211..b0296ce1f723 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -230,7 +230,16 @@ const CippSchedulerForm = (props) => { placeholder={`Enter a value for ${param.Name}`} validators={fieldRequired(param)} /> - ) : null} + ) : ( + + )} ))} From cbd0261f811dec264069dd2ea952eda5ec4ca3dc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 19:52:16 -0500 Subject: [PATCH 4/9] add filters --- src/pages/cipp/scheduler/index.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 18b0d01a89b5..97abcc20b602 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -34,6 +34,29 @@ const Page = () => { }, ]; + const filterList = [ + { + filterName: "Running", + value: [{ id: "TaskState", value: "Running" }], + type: "column", + }, + { + filterName: "Planned", + value: [{ id: "TaskState", value: "Planned" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "TaskState", value: "Failed" }], + type: "column", + }, + { + filterName: "Completed", + value: [{ id: "TaskState", value: "Completed" }], + type: "column", + }, + ]; + const offCanvas = { children: (extendedData) => ( <> @@ -75,6 +98,7 @@ const Page = () => { ]} actions={actions} offCanvas={offCanvas} + filters={filterList} /> ); }; From bde053a3d2729e2967a10d8ed42550612aa42ffc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 20:56:21 -0500 Subject: [PATCH 5/9] scheduler form fixes reduce rerenders on autocompletes remove transition animations memoize components --- .../CippComponents/CippAutocomplete.jsx | 62 ++- .../CippComponents/CippFormComponent.jsx | 24 +- .../CippFormPages/CippSchedulerForm.jsx | 382 ++++++++++-------- 3 files changed, 265 insertions(+), 203 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 1742ab9c17f2..7ce4c872f45a 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -6,12 +6,42 @@ import { TextField, IconButton, } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { ApiGetCallWithPagination } from "../../api/ApiCall"; import { Sync } from "@mui/icons-material"; import { Stack } from "@mui/system"; +import React from "react"; + +const MemoTextField = React.memo(function MemoTextField({ params, label, ...otherProps }) { + const { InputProps, ...otherParams } = params; + + return ( + + ); +}); export const CippAutoComplete = (props) => { const { @@ -136,6 +166,8 @@ export const CippAutoComplete = (props) => { } }, [api, actionGetRequest.data, actionGetRequest.isSuccess, actionGetRequest.isError]); + const memoizedOptions = useMemo(() => (api ? usedOptions : options), [api, usedOptions, options]); + const rand = Math.random().toString(36).substring(5); return ( @@ -143,7 +175,7 @@ export const CippAutoComplete = (props) => { key={`${defaultValue}-${rand}`} disabled={disabled || actionGetRequest.isFetching || isFetching} popupIcon={ - actionGetRequest.isFetching ? ( + actionGetRequest.isFetching || isFetching ? ( ) : ( @@ -216,24 +248,20 @@ export const CippAutoComplete = (props) => { onChange(newValue, newValue?.addedFields); } }} - options={api ? usedOptions : options} - getOptionLabel={(option) => - option - ? option.label === null - ? "" - : option.label || "Label not found - Are you missing a labelField?" - : "" - } + options={memoizedOptions} + getOptionLabel={useCallback( + (option) => + option + ? option.label === null + ? "" + : option.label || "Label not found - Are you missing a labelField?" + : "", + [] + )} sx={sx} renderInput={(params) => ( - + {api?.url && api?.showRefresh && ( { name={convertedName} control={formControl.control} rules={validators} - render={({ field }) => ( - field.onChange(value.value)} - /> - )} + render={({ field }) => + React.memo( + field.onChange(value.value)} + /> + ) + } /> diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index b0296ce1f723..03579b6cd2e4 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { Box, Button, Grid, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Box, Button, Divider, Grid, Skeleton, SvgIcon, Typography } from "@mui/material"; import { useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; @@ -112,200 +112,232 @@ const CippSchedulerForm = (props) => { commands.isSuccess, ]); + const advancedParameters = useWatch({ control: formControl.control, name: "advancedParameters" }); + + useEffect(() => { + if (advancedParameters === true) { + var schedulerValues = formControl.getValues("parameters"); + Object.keys(schedulerValues).forEach((key) => { + if (schedulerValues[key] === "" || schedulerValues[key] === null) { + delete schedulerValues[key]; + } + }); + const jsonString = JSON.stringify(schedulerValues, null, 2); + formControl.setValue("RawJsonParameters", jsonString); + } + }, [advancedParameters]); + const gridSize = fullWidth ? 12 : 4; // Adjust size based on fullWidth prop return ( - - {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( - - )} - - - - - - - + <> + + {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( + + )} + + + - - { - return { - label: command.Function, - value: command.Function, - addedFields: command, - }; - }) || [] - } - validators={{ - validate: (value) => { - if (!value) { - return "Please select a Command"; - } - return true; - }, - }} - /> - - - - - - - - {selectedCommand?.addedFields?.Synopsis && ( - - PowerShell Command: - - {selectedCommand.addedFields.Synopsis} - - + + + + + { + return { + label: command.Function, + value: command.Function, + addedFields: command, + }; + }) || [] + } + 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) => ( + {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") ? ( + + ) : ( + + )} + + + ))} + + + + + + - - {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={4} + placeholder={`Enter a JSON object`} + /> - ))} - - - - getCippValidator(value, "json"), - }} + type="autoComplete" + name="postExecution" + label="Post Execution Actions" formControl={formControl} - multiline - rows={4} - placeholder={`Enter a JSON object`} + multiple + creatable={false} + options={[ + { label: "Webhook", value: "Webhook" }, + { label: "Email", value: "Email" }, + { label: "PSA", value: "PSA" }, + ]} /> - - - - - - + + + - + + ); }; From b1931561f6721d530b4becb5bbc193d9c88ccdc5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 21:21:03 -0500 Subject: [PATCH 6/9] CippApiDialog memoize in CippDatatable disable confirm button after submission add new parameter allowResubmit (default false) --- .../CippComponents/CippApiDialog.jsx | 15 ++++++++++-- src/components/CippTable/CippDataTable.js | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 99c5bcc455c9..f8f7b00b57e6 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -17,11 +17,20 @@ export const CippApiDialog = (props) => { row = {}, relatedQueryKeys, dialogAfterEffect, + allowResubmit = false, ...other } = props; const router = useRouter(); const [addedFieldData, setAddedFieldData] = useState({}); const [partialResults, setPartialResults] = useState([]); + const [isFormSubmitted, setIsFormSubmitted] = useState(false); + + useEffect(() => { + if (createDialog.open) { + setIsFormSubmitted(false); + } + }, [createDialog.open]); + const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, @@ -103,6 +112,7 @@ export const CippApiDialog = (props) => { }; const tenantFilter = useSettings().currentTenant; const handleActionClick = (row, action, formData) => { + setIsFormSubmitted(true); if (action.multiPost === undefined) { action.multiPost = false; } @@ -304,9 +314,10 @@ export const CippApiDialog = (props) => { - + {console.log("isFormSubmitted", isFormSubmitted)} diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 005678d16998..bd95a9baa393 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -358,16 +358,19 @@ export const CippDataTable = (props) => { customComponent={offCanvas?.customComponent} {...offCanvas} /> - {actionData.ready && ( - - )} + {useMemo(() => { + if (!actionData.ready) return null; + return ( + + ); + }, [actionData.ready, createDialog, actionData.action, actionData.data, queryKey, title])} ); }; From db0e254d89c9ab6012508f51094aedeebd45fb6b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 21:54:39 -0500 Subject: [PATCH 7/9] fix form state changes on save --- src/components/CippSettings/CippCustomRoles.jsx | 7 ++----- src/pages/cipp/super-admin/function-offloading.js | 2 +- src/pages/cipp/super-admin/tenant-mode.js | 2 +- src/pages/tenant/gdap-management/offboarding.js | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/CippSettings/CippCustomRoles.jsx b/src/components/CippSettings/CippCustomRoles.jsx index 6387ae339a4f..a71d3f0b7ea2 100644 --- a/src/components/CippSettings/CippCustomRoles.jsx +++ b/src/components/CippSettings/CippCustomRoles.jsx @@ -67,10 +67,7 @@ export const CippCustomRoles = () => { queryKey: "customRoleList", }); - const { - data: { pages = [] } = {}, - isSuccess: tenantsSuccess, - } = ApiGetCallWithPagination({ + const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-AllTenantSelector", }); @@ -280,7 +277,7 @@ export const CippCustomRoles = () => { label: role.RowKey, value: role.RowKey, }))} - isLoading={customRoleListFetching} + isFetching={customRoleListFetching} refreshFunction={() => refetchCustomRoleList()} creatable={true} formControl={formControl} diff --git a/src/pages/cipp/super-admin/function-offloading.js b/src/pages/cipp/super-admin/function-offloading.js index 489785fa9274..904c5640f0db 100644 --- a/src/pages/cipp/super-admin/function-offloading.js +++ b/src/pages/cipp/super-admin/function-offloading.js @@ -31,7 +31,7 @@ const Page = () => { OffloadFunctions: execOffloadFunctions.data?.OffloadFunctions, }); } - }, [execOffloadFunctions.isSuccess]); + }, [execOffloadFunctions.isSuccess, execOffloadFunctions.data]); return ( { TenantMode: execPartnerMode.data?.TenantMode, }); } - }, [execPartnerMode.isSuccess]); + }, [execPartnerMode.isSuccess, execPartnerMode.data]); return ( { hideBackButton={true} hidePageType={true} postUrl="/api/ExecOffboardTenant" + resetForm={true} > From 1f046acecc83569c5b0b975b84e3817b3f035033 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 8 Feb 2025 21:56:05 -0500 Subject: [PATCH 8/9] prevent resubmit on cippformpage --- src/components/CippFormPages/CippFormPage.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx index 7db1575e9620..71b0cf5e7682 100644 --- a/src/components/CippFormPages/CippFormPage.jsx +++ b/src/components/CippFormPages/CippFormPage.jsx @@ -32,6 +32,7 @@ const CippFormPage = (props) => { hidePageType = false, hideTitle = false, hideSubmit = false, + allowResubmit = false, addedButtons, ...other } = props; @@ -42,7 +43,7 @@ const CippFormPage = (props) => { relatedQueryKeys: queryKey, }); - const { isValid } = useFormState({ control: formControl.control }); + const { isValid, isDirty } = useFormState({ control: formControl.control }); useEffect(() => { delete router.query.tenantFilter; @@ -133,7 +134,7 @@ const CippFormPage = (props) => { {addedButtons && addedButtons}