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 (
+
+ );
+};
+
+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"