From a9150982de38dde6dffc12608c620339d25ec7f3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 22 Jan 2025 22:12:36 -0500 Subject: [PATCH 1/5] Update index.jsx --- src/pages/identity/administration/users/user/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 3ed348320120..80cbef9983e4 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -18,6 +18,7 @@ import { useEffect, useState } from "react"; import { usePopover } from "../../../../../hooks/use-popover"; import { useDialog } from "../../../../../hooks/use-dialog"; import CippUserActions from "/src/components/CippComponents/CippUserActions"; +import { PencilIcon } from "@heroicons/react/24/outline"; const Page = () => { const popover = usePopover(); @@ -362,6 +363,7 @@ const Page = () => { hideTitle: true, actions: [ { + icon: , label: "Edit Group", link: "/identity/administration/groups/edit?groupId=[id]", }, From be59621813671dd41802cab0d1eec1a605b40a35 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 23 Jan 2025 13:44:56 -0500 Subject: [PATCH 2/5] Update index.jsx --- src/pages/identity/administration/users/user/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 80cbef9983e4..37f3d396924f 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -356,7 +356,7 @@ const Page = () => { cardLabelBox: { cardLabelBoxHeader: , }, - text: "Group Memberships", + text: "Groups", subtext: "List of groups the user is a member of", table: { title: "Group Memberships", @@ -384,10 +384,10 @@ const Page = () => { cardLabelBox: { cardLabelBoxHeader: , }, - text: "Roles", + text: "Admin Roles", subtext: "List of roles the user is a member of", table: { - title: "Role Memberships", + title: "Admin Roles", hideTitle: true, data: userMemberOf?.data?.Results.filter( (item) => item?.["@odata.type"] === "#microsoft.graph.directoryRole" From 9408080c0151c94dd1f36e5daff510da320e5cde Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 23 Jan 2025 13:46:27 -0500 Subject: [PATCH 3/5] table maintenance new superadmin page --- .../CippCards/CippPropertyListCard.jsx | 6 +- .../CippComponents/CippApiDialog.jsx | 17 +- src/layouts/config.js | 5 + src/pages/cipp/advanced/table-maintenance.js | 413 ++++++++++++++++++ 4 files changed, 436 insertions(+), 5 deletions(-) create mode 100644 src/pages/cipp/advanced/table-maintenance.js diff --git a/src/components/CippCards/CippPropertyListCard.jsx b/src/components/CippCards/CippPropertyListCard.jsx index b5952305fa79..ff7cdcd92a64 100644 --- a/src/components/CippCards/CippPropertyListCard.jsx +++ b/src/components/CippCards/CippPropertyListCard.jsx @@ -151,7 +151,11 @@ export const CippPropertyListCard = (props) => { action: item, ready: true, }); - createDialog.handleOpen(); + if (item?.noConfirm) { + item.customFunction(item, data, {}); + } else { + createDialog.handleOpen(); + } } } disabled={handleActionDisabled(data, item)} diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 282d8bab7b3b..70423a417045 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -38,6 +38,9 @@ export const CippApiDialog = (props) => { bulkRequest: api.multiPost === false, onResult: (result) => { setPartialResults((prevResults) => [...prevResults, result]); + if (api?.onSuccess) { + api.onSuccess(result); + } }, }); const actionGetRequest = ApiGetCall({ @@ -50,6 +53,9 @@ export const CippApiDialog = (props) => { bulkRequest: api.multiPost === false, onResult: (result) => { setPartialResults((prevResults) => [...prevResults, result]); + if (api?.onSuccess) { + api.onSuccess(result); + } }, }); @@ -58,6 +64,8 @@ export const CippApiDialog = (props) => { return api.dataFunction(row); } var newData = {}; + console.log("the received row", row); + console.log("the received dataObject", dataObject); if (api?.postEntireRow) { newData = row; @@ -85,6 +93,7 @@ export const CippApiDialog = (props) => { } }); } + console.log("output", newData); return newData; }; const tenantFilter = useSettings().currentTenant; @@ -209,10 +218,10 @@ export const CippApiDialog = (props) => { } useEffect(() => { if (api.noConfirm) { - formHook.handleSubmit(onSubmit)(); - createDialog.handleClose(); + formHook.handleSubmit(onSubmit)(); // Submits the form on mount + createDialog.handleClose(); // Closes the dialog after submitting } - }, [api.noConfirm]); + }, [api.noConfirm]); // Run effect only when api.noConfirm changes const handleClose = () => { createDialog.handleClose(); @@ -251,7 +260,7 @@ export const CippApiDialog = (props) => { Close diff --git a/src/layouts/config.js b/src/layouts/config.js index 39d5f0c8714e..7dceb0c4cf65 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -472,6 +472,11 @@ export const nativeMenuItems = [ path: "/cipp/advanced/timers", roles: ["superadmin"], }, + { + title: "Table Maintenance", + path: "/cipp/advanced/table-maintenance", + roles: ["superadmin"], + } ], }, ], diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js new file mode 100644 index 000000000000..d736115b1636 --- /dev/null +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -0,0 +1,413 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useEffect, useState } from "react"; +import { ApiPostCall } from "../../../api/ApiCall"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; // Fixed import +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; // Fixed import +import { useDialog } from "../../../hooks/use-dialog"; +import { + Box, + Container, + Stack, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + IconButton, + Button, + SvgIcon, + Tooltip, + Typography, +} from "@mui/material"; +import { MagnifyingGlassIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { Add, AddCircle, RemoveCircle, Sync, WarningAmber } from "@mui/icons-material"; +import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; +import { useForm, useWatch } from "react-hook-form"; +import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; +import { Grid } from "@mui/system"; + +const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultValues }) => { + const fields = useWatch({ control: formControl.control, name: "fields" }); + + useEffect(() => { + if (open) { + console.log(defaultValues); + formControl.reset({ + fields: defaultValues.fields || [], + }); + } + }, [open, defaultValues]); + + const addField = () => { + formControl.reset({ + fields: [...fields, { name: "", value: "" }], + }); + }; + + const removeField = (index) => { + const newFields = fields.filter((_, i) => i !== index); + formControl.reset({ fields: newFields }); + }; + + return ( + + Add/Edit Row + + + {Array.isArray(fields) && fields?.length > 0 && ( + <> + {fields.map((field, index) => ( + + + + + + + + removeField(index)}> + + + + ))} + + )} + + + + + + + + + ); +}; + +const Page = () => { + const pageTitle = "Table Maintenance"; + const apiUrl = "/api/ExecAzBobbyTables"; + const [tables, setTables] = useState([]); + const [selectedTable, setSelectedTable] = useState(null); + const [tableData, setTableData] = useState([]); + const addTableDialog = useDialog(); // Add dialog for adding table + const deleteTableDialog = useDialog(); // Add dialog for deleting table + const addEditRowDialog = useDialog(); // Add dialog for adding/editing row + const [defaultAddEditValues, setDefaultAddEditValues] = useState({}); + const formControl = useForm({ + mode: "onChange", + }); + + const addEditFormControl = useForm({ + mode: "onChange", + }); + + const tableFilter = useWatch({ control: formControl.control, name: "tableFilter" }); + + const fetchTables = ApiPostCall({ + queryKey: "CippTables", + onResult: (result) => setTables(result), + }); + + const fetchTableData = ApiPostCall({ + queryKey: "CippTableData", + onResult: (result) => { + setTableData(result); + }, + }); + + const handleTableSelect = (tableName) => { + setSelectedTable(tableName); + fetchTableData.mutate({ + url: apiUrl, + data: { + FunctionName: "Get-AzDataTableEntity", + TableName: tableName, + Parameters: { First: 1000 }, + }, + }); + }; + + const handleRefresh = () => { + if (selectedTable) { + fetchTableData.mutate({ + url: apiUrl, + data: { + FunctionName: "Get-AzDataTableEntity", + TableName: selectedTable, + Parameters: { First: 1000 }, + }, + }); + } + }; + + const tableRowAction = ApiPostCall({ + queryKey: "CippTableRowAction", + onResult: handleRefresh, + }); + + const handleTableRefresh = () => { + fetchTables.mutate({ url: apiUrl, data: { FunctionName: "Get-AzDataTable", Parameters: {} } }); + }; + + useEffect(() => { + handleTableRefresh(); + }, []); + + const actionItems = tables + .filter( + (table) => + tableFilter === "" || + tableFilter === undefined || + table.toLowerCase().includes(tableFilter.toLowerCase()) + ) + .map((table) => ({ + label: `${table}`, + customFunction: () => { + setTableData([]); + handleTableSelect(table); + }, + noConfirm: true, + })); + + const propertyItems = [ + { + label: "", + value: ( + + + + + + + ), + }, + ]; + + const getTableFields = () => { + if (tableData.length === 0) return []; + const sampleRow = tableData[0]; + return Object.keys(sampleRow) + .filter((key) => key !== "ETag" && key !== "Timestamp") + .map((key) => ({ + name: key, + label: key, + type: "textField", + required: false, + })); + }; + + return ( + + + {pageTitle} + + + + a.label.localeCompare(b.label))} + isFetching={fetchTables.isPending} + cardSx={{ maxHeight: "calc(100vh - 170px)", overflow: "auto" }} + actionButton={ + + + + + + + + + + + + + + + + + } + /> + + + {selectedTable && ( + + + + + + } + actions={[ + { + label: "Edit", + type: "POST", + icon: ( + + + + ), + customFunction: (row) => { + setDefaultAddEditValues({ + fields: Object.keys(row) + .filter((key) => key !== "ETag" && key !== "Timestamp") + .map((key) => ({ name: key, value: row[key] })), + }); + addEditRowDialog.handleOpen(); + }, + noConfirm: true, + }, + { + label: "Delete", + type: "POST", + icon: ( + + + + ), + url: apiUrl, + data: { + FunctionName: "Remove-AzDataTableEntity", + TableName: `!${selectedTable}`, + Parameters: { + Entity: { RowKey: "RowKey", PartitionKey: "PartitionKey", ETag: "ETag" }, + }, + }, + onSuccess: handleRefresh, + confirmText: "Do you want to delete this row?", + }, + ]} + /> + + )} + + + { + handleTableRefresh(); + }, + }} + /> + + + + Are you sure you want to delete this table? This is a destructive action that cannot + be undone. + + + ), + type: "POST", + data: { FunctionName: "Remove-AzDataTable", TableName: selectedTable, Parameters: {} }, + onSuccess: () => { + setSelectedTable(null); + setTableData([]); + handleTableRefresh(); + }, + }} + /> + { + const payload = data.fields.reduce((acc, field) => { + acc[field.name] = field.value; + return acc; + }, {}); + tableRowAction.mutate({ + url: apiUrl, + data: { + FunctionName: "Add-AzDataTableEntity", + TableName: selectedTable, + Parameters: { Entity: payload, Force: true }, + }, + onSuccess: handleRefresh, + }); + addEditRowDialog.handleClose(); + }} + defaultValues={defaultAddEditValues} + /> + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 0c210ba488bae7f55c8f61553a67416bc7276544 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 23 Jan 2025 14:07:32 -0500 Subject: [PATCH 4/5] field types --- src/pages/cipp/advanced/table-maintenance.js | 71 ++++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index d736115b1636..20e7594c9f64 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -17,6 +17,8 @@ import { SvgIcon, Tooltip, Typography, + MenuItem, + Select, } from "@mui/material"; import { MagnifyingGlassIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Add, AddCircle, RemoveCircle, Sync, WarningAmber } from "@mui/icons-material"; @@ -30,16 +32,15 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV useEffect(() => { if (open) { - console.log(defaultValues); formControl.reset({ fields: defaultValues.fields || [], }); } - }, [open, defaultValues]); + }, [open, defaultValues, formControl]); const addField = () => { formControl.reset({ - fields: [...fields, { name: "", value: "" }], + fields: [...fields, { name: "", value: "", type: "textField" }], }); }; @@ -48,6 +49,11 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV formControl.reset({ fields: newFields }); }; + const handleTypeChange = (index, newType) => { + const newFields = fields.map((field, i) => (i === index ? { ...field, type: newType } : field)); + formControl.reset({ fields: newFields }); + }; + return ( Add/Edit Row @@ -65,14 +71,36 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV label="Name" /> - + + + + { + if (field.type === "switch") { + return { ml: 2 }; + } else if (field.type === "number") { + return { width: "100%" }; + } else { + return {}; + } + }} /> + removeField(index)}> @@ -203,12 +231,21 @@ const Page = () => { const sampleRow = tableData[0]; return Object.keys(sampleRow) .filter((key) => key !== "ETag" && key !== "Timestamp") - .map((key) => ({ - name: key, - label: key, - type: "textField", - required: false, - })); + .map((key) => { + const value = sampleRow[key]; + let type = "textField"; + if (typeof value === "number") { + type = "number"; + } else if (typeof value === "boolean") { + type = "switch"; + } + return { + name: key, + label: key, + type: type, + required: false, + }; + }); }; return ( @@ -273,6 +310,7 @@ const Page = () => { fields: getTableFields().map((field) => ({ name: field?.name, value: "", + type: field?.type, })), }); addEditRowDialog.handleOpen(); @@ -313,7 +351,16 @@ const Page = () => { setDefaultAddEditValues({ fields: Object.keys(row) .filter((key) => key !== "ETag" && key !== "Timestamp") - .map((key) => ({ name: key, value: row[key] })), + .map((key) => { + const value = row[key]; + let type = "textField"; + if (typeof value === "number") { + type = "number"; + } else if (typeof value === "boolean") { + type = "switch"; + } + return { name: key, value: value, type: type }; + }), }); addEditRowDialog.handleOpen(); }, From 8782844c32e8dabdc7beb5d3530efb362ac0d40f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 23 Jan 2025 15:53:09 -0500 Subject: [PATCH 5/5] table maintenance tweaks add filters --- src/pages/cipp/advanced/table-maintenance.js | 104 +++++++++++++++++-- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index 20e7594c9f64..df6592df96f3 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -19,6 +19,7 @@ import { Typography, MenuItem, Select, + Alert, } from "@mui/material"; import { MagnifyingGlassIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Add, AddCircle, RemoveCircle, Sync, WarningAmber } from "@mui/icons-material"; @@ -26,6 +27,7 @@ import CippFormComponent from "../../../components/CippComponents/CippFormCompon import { useForm, useWatch } from "react-hook-form"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { Grid } from "@mui/system"; +import CippButtonCard from "../../../components/CippCards/CippButtonCard"; const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultValues }) => { const fields = useWatch({ control: formControl.control, name: "fields" }); @@ -114,8 +116,12 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV - - + + ); @@ -131,14 +137,23 @@ const Page = () => { const deleteTableDialog = useDialog(); // Add dialog for deleting table const addEditRowDialog = useDialog(); // Add dialog for adding/editing row const [defaultAddEditValues, setDefaultAddEditValues] = useState({}); + const [tableFilterParams, setTableFilterParams] = useState({ First: 1000 }); const formControl = useForm({ mode: "onChange", }); + const [accordionExpanded, setAccordionExpanded] = useState(false); const addEditFormControl = useForm({ mode: "onChange", }); + const filterFormControl = useForm({ + mode: "onChange", + defaultValues: { + First: 1000, + }, + }); + const tableFilter = useWatch({ control: formControl.control, name: "tableFilter" }); const fetchTables = ApiPostCall({ @@ -160,7 +175,7 @@ const Page = () => { data: { FunctionName: "Get-AzDataTableEntity", TableName: tableName, - Parameters: { First: 1000 }, + Parameters: filterFormControl.getValues(), }, }); }; @@ -172,7 +187,7 @@ const Page = () => { data: { FunctionName: "Get-AzDataTableEntity", TableName: selectedTable, - Parameters: { First: 1000 }, + Parameters: tableFilterParams, }, }); } @@ -187,6 +202,20 @@ const Page = () => { fetchTables.mutate({ url: apiUrl, data: { FunctionName: "Get-AzDataTable", Parameters: {} } }); }; + const getSelectedProps = (data) => { + if (data?.Property && data?.Property.length > 0) { + var selectedProps = ["ETag", "PartitionKey", "RowKey"]; + data?.Property.map((prop) => { + if (selectedProps.indexOf(prop.value) === -1) { + selectedProps.push(prop.value); + } + }); + return selectedProps; + } else { + return []; + } + }; + useEffect(() => { handleTableRefresh(); }, []); @@ -211,16 +240,11 @@ const Page = () => { { label: "", value: ( - + - + ), }, @@ -253,6 +277,10 @@ const Page = () => { {pageTitle} + + This page allows you to view and manage data in Azure Tables. This is advanced functionality + that should only be used when directed by CyberDrain support. + { {selectedTable && ( + { + var properties = getSelectedProps(data); + setTableFilterParams({ ...data, Property: properties }); + handleRefresh(); + setAccordionExpanded(false); + })} + > + Apply Filters + + } + > + + + ({ + label: field?.label, + value: field?.name, + }))} + /> + + + + + +