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..df6592df96f3 --- /dev/null +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -0,0 +1,542 @@ +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, + 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"; +import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; +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" }); + + useEffect(() => { + if (open) { + formControl.reset({ + fields: defaultValues.fields || [], + }); + } + }, [open, defaultValues, formControl]); + + const addField = () => { + formControl.reset({ + fields: [...fields, { name: "", value: "", type: "textField" }], + }); + }; + + const removeField = (index) => { + const newFields = fields.filter((_, i) => i !== index); + 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 + + + {Array.isArray(fields) && fields?.length > 0 && ( + <> + {fields.map((field, index) => ( + + + + + + + + + { + if (field.type === "switch") { + return { ml: 2 }; + } else if (field.type === "number") { + return { width: "100%" }; + } else { + return {}; + } + }} + /> + + + 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 [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({ + 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: filterFormControl.getValues(), + }, + }); + }; + + const handleRefresh = () => { + if (selectedTable) { + fetchTableData.mutate({ + url: apiUrl, + data: { + FunctionName: "Get-AzDataTableEntity", + TableName: selectedTable, + Parameters: tableFilterParams, + }, + }); + } + }; + + const tableRowAction = ApiPostCall({ + queryKey: "CippTableRowAction", + onResult: handleRefresh, + }); + + const handleTableRefresh = () => { + 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(); + }, []); + + 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) => { + 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 ( + + + {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. + + + + a.label.localeCompare(b.label))} + isFetching={fetchTables.isPending} + cardSx={{ maxHeight: "calc(100vh - 170px)", overflow: "auto" }} + actionButton={ + + + + + + + + + + + + + + + + + } + /> + + + {selectedTable && ( + + { + var properties = getSelectedProps(data); + setTableFilterParams({ ...data, Property: properties }); + handleRefresh(); + setAccordionExpanded(false); + })} + > + Apply Filters + + } + > + + + ({ + label: field?.label, + value: field?.name, + }))} + /> + + + + + + + + + + + } + actions={[ + { + label: "Edit", + type: "POST", + icon: ( + + + + ), + customFunction: (row) => { + setDefaultAddEditValues({ + fields: Object.keys(row) + .filter((key) => key !== "ETag" && key !== "Timestamp") + .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(); + }, + 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; diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 3ed348320120..37f3d396924f 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(); @@ -355,13 +356,14 @@ const Page = () => { cardLabelBox: { cardLabelBoxHeader: , }, - text: "Group Memberships", + text: "Groups", subtext: "List of groups the user is a member of", table: { title: "Group Memberships", hideTitle: true, actions: [ { + icon: , label: "Edit Group", link: "/identity/administration/groups/edit?groupId=[id]", }, @@ -382,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"