Skip to content

Commit

Permalink
Merge pull request #3463 from JohnDuprey/dev
Browse files Browse the repository at this point in the history
view user improvements
  • Loading branch information
JohnDuprey authored Jan 25, 2025
2 parents d582e31 + 4bc4026 commit 3c16899
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 17 deletions.
26 changes: 17 additions & 9 deletions src/components/CippCards/CippBannerListCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CippPropertyListCard } from "./CippPropertyListCard";
import { CippDataTable } from "../CippTable/CippDataTable";

export const CippBannerListCard = (props) => {
const { items = [], isCollapsible = false, isFetching = false, ...other } = props;
const { items = [], isCollapsible = false, isFetching = false, children, ...other } = props;
const [expanded, setExpanded] = useState(null);

const handleExpand = useCallback((itemId) => {
Expand Down Expand Up @@ -145,14 +145,18 @@ export const CippBannerListCard = (props) => {
{isCollapsible && (
<Collapse in={isExpanded}>
<Divider />
{item?.propertyItems?.length > 0 && (
<CippPropertyListCard
propertyItems={item.propertyItems || []}
layout="dual"
isFetching={item.isFetching || false}
/>
)}
{item?.table && <CippDataTable {...item.table} />}
<Stack spacing={1}>
{item?.propertyItems?.length > 0 && (
<CippPropertyListCard
propertyItems={item.propertyItems || []}
layout="dual"
isFetching={item.isFetching || false}
/>
)}
{item?.table && <CippDataTable {...item.table} />}
{item?.children && <Box sx={{ pl: 3 }}>{item.children}</Box>}
{item?.actionButton && <Box sx={{ pl: 3, pb: 2 }}>{item.actionButton}</Box>}
</Stack>
</Collapse>
)}
</li>
Expand Down Expand Up @@ -180,8 +184,12 @@ CippBannerListCard.propTypes = {
subtext: PropTypes.string,
statusColor: PropTypes.string,
statusText: PropTypes.string,
actionButton: PropTypes.element,
propertyItems: PropTypes.array,
table: PropTypes.object,
actionButton: PropTypes.element,
isFetching: PropTypes.bool,
children: PropTypes.node,
})
).isRequired,
isCollapsible: PropTypes.bool,
Expand Down
52 changes: 52 additions & 0 deletions src/components/CippComponents/CippLocationDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";
import { useState } from "react";
import dynamic from "next/dynamic"; // Import dynamic from next/dynamic
import { CippPropertyList } from "./CippPropertyList"; // Import CippPropertyList
import { LocationOn } from "@mui/icons-material";

const CippMap = dynamic(() => import("./CippMap"), { ssr: false }); // Dynamic import for CippMap

export const CippLocationDialog = ({ location }) => {
const [open, setOpen] = useState(false);

const handleOpen = () => {
setOpen(true);
};

const handleClose = () => {
setOpen(false);
};

const markers = [
{
position: [location.geoCoordinates.latitude, location.geoCoordinates.longitude],
popup: `${location.city}, ${location.state}, ${location.countryOrRegion}`,
},
];

const properties = [
{ label: "City", value: location.city },
{ label: "State", value: location.state },
{ label: "Country/Region", value: location.countryOrRegion },
];

return (
<>
<Button size="small" variant="outlined" onClick={handleOpen} startIcon={<LocationOn />}>
Show Map
</Button>
<Dialog fullWidth maxWidth="sm" onClose={handleClose} open={open}>
<DialogTitle>Location Details</DialogTitle>
<DialogContent>
<CippPropertyList propertyItems={properties} showDivider={false} />
<CippMap markers={markers} zoom={10} mapSx={{ height: "300px", width: "100%" }} />
</DialogContent>
<DialogActions>
<Button color="inherit" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
</>
);
};
9 changes: 7 additions & 2 deletions src/components/CippTable/util-columnsFromAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ const mergeKeys = (dataArray) => {

export const utilColumnsFromAPI = (dataArray) => {
const dataSample = mergeKeys(dataArray);

const skipRecursion = ["location"];
const generateColumns = (obj, parentKey = "") => {
return Object.keys(obj)
.map((key) => {
const accessorKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
if (
typeof obj[key] === "object" &&
obj[key] !== null &&
!Array.isArray(obj[key]) &&
!skipRecursion.includes(key)
) {
return generateColumns(obj[key], accessorKey);
}

Expand Down
17 changes: 17 additions & 0 deletions src/data/signinErrorCodes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"0": "Success",
"50126": "Invalid username or password",
"70044": "The session has expired or is invalid due to sign-in frequency checks by conditional access",
"50089": "Flow token expired",
"53003": "Access has been blocked by Conditional Access policies",
"50140": "This error occurred due to 'Keep me signed in' interrupt when the user was signing-in",
"50097": "Device authentication required",
"65001": "Application X doesn't have permission to access application Y or the permission has been revoked",
"50053": "Account is locked because user tried to sign in too many times with an incorrect user ID or password",
"50020": "The user is unauthorized",
"50125": "Sign-in was interrupted due to a password reset or password registration entry",
"50074": "User did not pass the MFA challenge",
"50133": "Session is invalid due to expiration or recent password change",
"530002": "Your device is required to be compliant to access this resource",
"9001011": "Device policy contains unsupported required device state"
}
111 changes: 105 additions & 6 deletions src/pages/identity/administration/users/user/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,68 @@ import { CippCopyToClipBoard } from "../../../../../components/CippComponents/Ci
import { Box, Stack } from "@mui/system";
import Grid from "@mui/material/Grid2";
import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard";
import { Typography } from "@mui/material";
import { SvgIcon, Typography } from "@mui/material";
import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard";
import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo";
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";
import { EyeIcon, PencilIcon } from "@heroicons/react/24/outline";
import { CippDataTable } from "/src/components/CippTable/CippDataTable";
import dynamic from "next/dynamic";
const CippMap = dynamic(() => import("/src/components/CippComponents/CippMap"), { ssr: false });

import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material";
import { Close } from "@mui/icons-material";
import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList";

const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle sx={{ py: 2 }}>
Sign-In Logs
<IconButton
aria-label="close"
onClick={onClose}
sx={{ position: "absolute", right: 8, top: 8 }}
>
<Close />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<CippDataTable
noCard={true}
title="Sign-In Logs"
simpleColumns={[
"createdDateTime",
"status",
"ipAddress",
"clientAppUsed",
"resourceDisplayName",
"status.errorCode",
"location",
]}
api={{
url: "/api/ListUserSigninLogs",
data: {
UserId: userId,
tenantFilter: tenantFilter,
top: 50,
},
queryKey: `ListSignIns-${userId}`,
}}
/>
</DialogContent>
</Dialog>
);
};

const Page = () => {
const popover = usePopover();
const createDialog = useDialog();
const userSettingsDefaults = useSettings();
const router = useRouter();
const { userId } = router.query;
const [waiting, setWaiting] = useState(false);
const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false);

useEffect(() => {
if (userId) {
setWaiting(true);
Expand Down Expand Up @@ -112,6 +158,20 @@ const Page = () => {
subtext: `Logged into application ${signInData.resourceDisplayName || "Unknown Application"}`,
statusColor: signInData.status.errorCode === 0 ? "success.main" : "error.main",
statusText: signInData.status.errorCode === 0 ? "Success" : "Failed",
actionButton: (
<Button
variant="contained"
size="small"
onClick={() => setSignInLogsDialogOpen(true)}
startIcon={
<SvgIcon fontSize="small">
<EyeIcon />
</SvgIcon>
}
>
More Sign-In Logs
</Button>
),
propertyItems: [
{
label: "Client App Used",
Expand All @@ -131,6 +191,39 @@ const Page = () => {
value: signInData.status?.additionalDetails || "N/A",
},
],
children: (
<>
{signInData?.location && (
<>
<Typography variant="h6">Location</Typography>
<Grid container spacing={2}>
<Grid item size={8}>
<CippMap
markers={[
{
position: [
signInData.location.geoCoordinates.latitude,
signInData.location.geoCoordinates.longitude,
],
popup: `${signInData.location.city}, ${signInData.location.state}, ${signInData.location.countryOrRegion}`,
},
]}
/>
</Grid>
<Grid item size={4}>
<CippPropertyList
propertyItems={[
{ label: "City", value: signInData.location.city },
{ label: "State", value: signInData.location.state },
{ label: "Country/Region", value: signInData.location.countryOrRegion },
]}
/>
</Grid>
</Grid>
</>
)}
</>
),
};

// Prepare the conditional access policies items
Expand Down Expand Up @@ -459,6 +552,12 @@ const Page = () => {
</Grid>
</Box>
)}
<SignInLogsDialog
open={signInLogsDialogOpen}
onClose={() => setSignInLogsDialogOpen(false)}
userId={userId}
tenantFilter={userSettingsDefaults.currentTenant}
/>
</HeaderedTabbedLayout>
);
};
Expand Down
82 changes: 82 additions & 0 deletions src/pages/identity/reports/signin-report/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import { useState } from "react";
import { Button, Grid, TextField, Switch, FormControlLabel } from "@mui/material";
import CippButtonCard from "/src/components/CippCards/CippButtonCard";

const Page = () => {
const pageTitle = "Sign Ins Report";
Expand All @@ -16,14 +19,93 @@ const Page = () => {
"locationcipp",
];

const [filterValues, setFilterValues] = useState({
Days: 7,
filter: "",
failedLogonsOnly: false,
FailureThreshold: 0,
});

const [appliedFilters, setAppliedFilters] = useState(filterValues);

const handleFilterChange = (event) => {
const { name, value, type, checked } = event.target;
setFilterValues({
...filterValues,
[name]: type === "checkbox" ? checked : value,
});
};

const handleFilterSubmit = () => {
setAppliedFilters(filterValues);
};

const tableFilter = (
<CippButtonCard title="Filter Options" component="accordion">
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
label="Days"
name="Days"
type="number"
value={filterValues.Days}
onChange={handleFilterChange}
fullWidth
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="Custom Filter"
name="filter"
value={filterValues.filter}
onChange={handleFilterChange}
fullWidth
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Switch
checked={filterValues.failedLogonsOnly}
onChange={handleFilterChange}
name="failedLogonsOnly"
/>
}
label="Failed Logons Only"
/>
</Grid>
{filterValues.failedLogonsOnly && (
<Grid item xs={12} sm={6}>
<TextField
label="Failure Threshold"
name="FailureThreshold"
type="number"
value={filterValues.FailureThreshold}
onChange={handleFilterChange}
fullWidth
/>
</Grid>
)}
<Grid item xs={12}>
<Button variant="contained" color="primary" onClick={handleFilterSubmit}>
Apply Filter
</Button>
</Grid>
</Grid>
</CippButtonCard>
);

return (
<>
<CippTablePage
tableFilter={tableFilter}
title={pageTitle}
apiUrl={apiUrl}
apiData={appliedFilters}
actions={actions}
offCanvas={offCanvas}
simpleColumns={simpleColumns}
queryKey={`ListSignIns-${JSON.stringify(appliedFilters)}`}
/>
</>
);
Expand Down
Loading

0 comments on commit 3c16899

Please sign in to comment.