Skip to content

Commit

Permalink
[TM-857] add basic structure for audit log (#249)
Browse files Browse the repository at this point in the history
* add basic structure for audit log

---------

Co-authored-by: Limber Mamani Vallejos <[email protected]>
Co-authored-by: Dotty <[email protected]>
  • Loading branch information
3 people authored Jun 17, 2024
1 parent 56c12c0 commit b71d6cd
Show file tree
Hide file tree
Showing 34 changed files with 1,370 additions and 164 deletions.
2 changes: 1 addition & 1 deletion src/admin/apiProvider/utils/entryFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const isDateType = (value: any) => {
return isValid(parseISO(value));
};

const convertDateFormat = (value: any) => {
export const convertDateFormat = (value: any) => {
if (typeof value === "string") {
const dateObject = new Date(value);
const formattedDay = dateObject.getUTCDate().toString().padStart(2, "0");
Expand Down
162 changes: 86 additions & 76 deletions src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,104 @@
import { Typography } from "@mui/material";
import { FC } from "react";
import {
Datagrid,
DateField,
FunctionField,
Pagination,
ReferenceField,
ReferenceManyField,
TabbedShowLayout,
TabProps,
useShowContext
} from "react-admin";
import { Grid, Stack } from "@mui/material";
import { FC, useEffect, useState } from "react";
import { Button, Link, TabbedShowLayout, TabProps, useBasename, useShowContext } from "react-admin";
import { When } from "react-if";

import modules from "@/admin/modules";
import { V2AdminUserRead } from "@/generated/apiSchemas";
import { Entity } from "@/types/common";
import Text from "@/components/elements/Text/Text";
import { PROJECT, SITE } from "@/constants/entities";
import useAuditLogActions from "@/hooks/AuditStatus/useAuditLogActions";

import AuditLogSiteTabSelection from "./components/AuditLogSiteTabSelection";
import SiteAuditLogEntityStatus from "./components/SiteAuditLogEntityStatus";
import SiteAuditLogEntityStatusSide from "./components/SiteAuditLogEntityStatusSide";
import SiteAuditLogProjectStatus from "./components/SiteAuditLogProjectStatus";
import { AuditLogButtonStates } from "./constants/enum";

interface IProps extends Omit<TabProps, "label" | "children"> {
label?: string;
entity?: Entity["entityName"];
}

interface FeedbackProps {
comment: string | undefined;
}
const AuditLogTab: FC<IProps> = ({ label, ...rest }) => {
const [buttonToogle, setButtonToogle] = useState(AuditLogButtonStates.PROJECT);
const { record, isLoading } = useShowContext();
const basename = useBasename();

const Feedback: FC<FeedbackProps> = ({ comment }) => {
if (comment == null) {
return <>-</>;
}
const {
mutateEntity,
valuesForStatus,
statusLabels,
entityType,
entityListItem,
loadEntityList,
selected,
setSelected,
auditLogData,
refetch,
checkPolygonsSite
} = useAuditLogActions({
record,
buttonToogle,
entityLevel: record?.project ? SITE : PROJECT
});

return (
<>
{comment.split("\n").map(fragment => (
<>
{fragment}
<br />
</>
))}
</>
);
};

const AuditLogTab: FC<IProps> = ({ label, entity, ...rest }) => {
const ctx = useShowContext();
const resource = entity ?? ctx.resource;
useEffect(() => {
refetch();
loadEntityList();
}, [buttonToogle]);

return (
<When condition={!ctx.isLoading}>
<When condition={!isLoading}>
<TabbedShowLayout.Tab label={label ?? "Audit log"} {...rest}>
<Typography variant="h5" component="h3">
Audit Log
</Typography>
<ReferenceManyField
pagination={<Pagination />}
reference={modules.audit.ResourceName}
filter={{ entity: resource }}
target="uuid"
label=""
>
<Datagrid bulkActionButtons={false}>
<DateField
source="created_at"
label="Date and time"
showTime
locales="en-GB"
options={{ dateStyle: "short", timeStyle: "short" }}
/>
<ReferenceField source="user_uuid" reference={modules.user.ResourceName} label="User">
<FunctionField
source="first_name"
render={(record: V2AdminUserRead) => `${record?.first_name || ""} ${record?.last_name || ""}`}
/>
</ReferenceField>
<FunctionField
label="Action"
className="capitalize"
render={(record: any) => {
const str: string = record?.new_values?.status ?? record?.event ?? "";

return str.replaceAll("-", " ");
<Grid spacing={2} container className="max-h-[200vh] overflow-auto">
<Grid xs={8}>
<Stack gap={4} className="pl-8 pt-9">
<AuditLogSiteTabSelection buttonToogle={buttonToogle} setButtonToogle={setButtonToogle} />
<When condition={buttonToogle === AuditLogButtonStates.PROJECT && record?.project}>
<Text variant="text-24-bold">Project Status</Text>
<Text variant="text-14-light" className="mb-4">
Update the site status, view updates, or add comments
</Text>
<Button
className="!mb-[25vh] !w-2/5 !rounded-lg !border-2 !border-solid !border-primary-500 !bg-white !px-4 !py-[10.5px] !text-xs !font-bold !uppercase !leading-[normal] !text-primary-500 hover:!bg-grey-900 disabled:!border-transparent disabled:!bg-grey-750 disabled:!text-grey-730 lg:!mb-[40vh] lg:!text-sm wide:!text-base"
component={Link}
to={`${basename}/${modules.project.ResourceName}/${record?.project?.uuid}/show/5`}
fullWidth
label="OPEN PROJECT AUDIT LOG"
/>
</When>
<When condition={buttonToogle === AuditLogButtonStates.PROJECT && !record?.project}>
<SiteAuditLogProjectStatus record={record} auditLogData={auditLogData} />
</When>
<When condition={buttonToogle !== AuditLogButtonStates.PROJECT}>
<SiteAuditLogEntityStatus
entityType={entityType}
record={selected}
auditLogData={auditLogData}
refresh={refetch}
buttonToogle={buttonToogle}
/>
</When>
</Stack>
</Grid>
<Grid xs={4} className="pl-8 pr-4 pt-9">
<SiteAuditLogEntityStatusSide
getValueForStatus={valuesForStatus}
progressBarLabels={statusLabels}
mutate={mutateEntity}
entityType={entityType}
refresh={() => {
refetch();
loadEntityList();
}}
record={selected}
polygonList={entityListItem}
selectedPolygon={selected}
setSelectedPolygon={setSelected}
auditLogData={auditLogData?.data}
checkPolygonsSite={checkPolygonsSite}
/>
<FunctionField
label="Comments"
render={(record: any) => <Feedback comment={record?.new_values?.feedback} />}
/>
</Datagrid>
</ReferenceManyField>
</Grid>
</Grid>
</TabbedShowLayout.Tab>
</When>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FC } from "react";

import Button from "@/components/elements/Button/Button";

interface AuditLogSiteTabSelectionProps {
buttonToogle: number;
setButtonToogle: (buttonToogle: number) => void;
}

const tabNames = ["Project Status", "Site Status", "Polygon Status"];

const AuditLogSiteTabSelection: FC<AuditLogSiteTabSelectionProps> = ({ buttonToogle, setButtonToogle }) => (
<div className="flex w-fit gap-1 rounded-lg bg-neutral-200 p-1">
{tabNames.map((tabName, index) => (
<Button
key={index}
variant={`${buttonToogle === index ? "white-toggle" : "transparent-toggle"}`}
onClick={() => setButtonToogle(index)}
>
{tabName}
</Button>
))}
</div>
);

export default AuditLogSiteTabSelection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FC, Fragment } from "react";

import { convertDateFormat } from "@/admin/apiProvider/utils/entryFormat";
import Text from "@/components/elements/Text/Text";
import { AuditStatusResponse, V2FileRead } from "@/generated/apiSchemas";

const formattedTextStatus = (text: string) => {
return text.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
};

const getTextForActionTable = (item: { type: string; status: string; request_removed: boolean }): string => {
if (item.type === "comment") {
return "New Comment";
} else if (item.type === "status") {
return `New Status: ${formattedTextStatus(item.status)}`;
} else if (item.request_removed) {
return "Change Request Removed";
} else {
return "Change Requested Added";
}
};

const columnTitles = ["Date", "User", "Action", "Comments", "Attachments"];

const AuditLogTable: FC<{ auditLogData: { data: AuditStatusResponse[] } }> = ({ auditLogData }) => (
<>
<div className="grid grid-cols-[14%_20%_15%_30%_21%]">
{columnTitles.map(title => (
<Text key={title} variant="text-12-light" className="border-b border-b-grey-750 text-grey-700">
{title}
</Text>
))}
</div>
<div className="mr-[-7px] grid max-h-[50vh] min-h-[10vh] grid-cols-[14%_20%_15%_30%_21%] overflow-auto pr-[7px]">
{auditLogData?.data?.map((item: AuditStatusResponse, index: number) => (
<Fragment key={index}>
<Text variant="text-12" className="border-b border-b-grey-750 py-2 pr-2">
{convertDateFormat(item?.date_created)}
</Text>
<Text variant="text-12" className="border-b border-b-grey-750 py-2 pr-2">
{`${item.first_name} ${item.last_name}`}
</Text>
<Text variant="text-12" className="border-b border-b-grey-750 py-2 pr-2">
{getTextForActionTable(item as { type: string; status: string; request_removed: boolean })}
</Text>
<Text variant="text-12" className="border-b border-b-grey-750 py-2">
{item.comment ?? "-"}
</Text>
<div className="grid max-w-full gap-2 gap-y-1 border-b border-b-grey-750 py-2">
{item?.attachments?.map((attachmentItem: V2FileRead) => (
<Text
key={attachmentItem.uuid}
variant="text-12-light"
className="h-min w-fit max-w-full cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap rounded-xl bg-neutral-40 px-2 py-0.5"
as={"span"}
onClick={() => {
attachmentItem.url && window.open(attachmentItem.url, "_blank");
}}
>
{attachmentItem.file_name}
</Text>
))}
</div>
</Fragment>
))}
</div>
</>
);

export default AuditLogTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FC } from "react";
import { Link as RaLink, useBasename } from "react-admin";
import { When } from "react-if";

import modules from "@/admin/modules";
import Text from "@/components/elements/Text/Text";
import { AuditStatusResponse } from "@/generated/apiSchemas";

import CommentarySection from "../../PolygonReviewTab/components/CommentarySection/CommentarySection";
import { AuditLogButtonStates } from "../constants/enum";
import { AuditLogEntity } from "../constants/types";
import AuditLogTable from "./AuditLogTable";

export interface SiteAuditLogEntityStatusProps {
entityType: AuditLogEntity;
record: SelectedItem | null;
auditLogData?: { data: AuditStatusResponse[] };
refresh: () => void;
buttonToogle: number;
}

interface SelectedItem {
title?: string | undefined;
name?: string | undefined;
uuid?: string | undefined;
value?: string | undefined;
meta?: string | undefined;
status?: string | undefined;
}

const SiteAuditLogEntityStatus: FC<SiteAuditLogEntityStatusProps> = ({
entityType,
record,
auditLogData,
refresh,
buttonToogle
}) => {
const isSite = buttonToogle === AuditLogButtonStates.SITE;
const basename = useBasename();

const getTitle = () => record?.title ?? record?.name;

return (
<div className="flex flex-col gap-6">
<div>
<Text variant="text-24-bold" className="mb-1">
{entityType} Status and Comments
</Text>
<Text variant="text-14-light" className="mb-4">
Update the {entityType?.toLowerCase()} status, view updates, or add comments
</Text>
<CommentarySection record={record} entity={entityType} refresh={refresh} viewCommentsList={false} />
</div>
<div>
{!isSite && <Text variant="text-16-bold">History and Discussion for {getTitle()}</Text>}
{isSite && (
<Text variant="text-16-bold">
<RaLink
className="text-16-bold !text-[#000000DD]"
to={`${basename}/${modules.site.ResourceName}/${record?.uuid}/show/6`}
>
{getTitle()}
</RaLink>
</Text>
)}
</div>
<When condition={!!auditLogData}>
<AuditLogTable auditLogData={auditLogData!} />
</When>
</div>
);
};

export default SiteAuditLogEntityStatus;
Loading

0 comments on commit b71d6cd

Please sign in to comment.