Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TM-857] add basic structure for audit log #249

Merged
merged 31 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09475a4
add basic structure for audit log
pachonjcl Jun 10, 2024
2baadae
add conditional to entity level
LimberHope Jun 10, 2024
2cd57b5
fix useloadEntityList hook
LimberHope Jun 10, 2024
ecd5669
add open project audit log button
LimberHope Jun 10, 2024
6bdbaa0
add change status functionality
LimberHope Jun 10, 2024
3cbfe61
change usePostV2AuditStatus -> usePostV2AuditStatusENTITYUUID
LimberHope Jun 11, 2024
f481162
change uuid -> site
LimberHope Jun 11, 2024
9f7057b
Merge branch 'staging' into epic/audit-log
pachonjcl Jun 11, 2024
7f7fcd9
update stories tests
pachonjcl Jun 11, 2024
3676ac0
change const with enum
pachonjcl Jun 12, 2024
4a557c7
removed unused interface
pachonjcl Jun 12, 2024
cd50daa
better type usage
pachonjcl Jun 12, 2024
8cf0894
update type in component
pachonjcl Jun 12, 2024
ead04cf
remove duplicated component
pachonjcl Jun 12, 2024
17b07c4
change audit log table titles to be dynamic
pachonjcl Jun 12, 2024
7dac0c1
remove unused param
pachonjcl Jun 12, 2024
839e8a9
Improve the usage of types for texts
pachonjcl Jun 12, 2024
f98f025
use when instead of conditional
pachonjcl Jun 12, 2024
7807576
rename variables and delete comments
pachonjcl Jun 12, 2024
7a38c32
change tabs to be dynamic
pachonjcl Jun 12, 2024
5418730
remove extra conditional
pachonjcl Jun 12, 2024
01f1627
fix some undefined errors
pachonjcl Jun 12, 2024
26a8f55
Merge branch 'staging' into epic/audit-log
pachonjcl Jun 12, 2024
11310af
fix some errors
LimberHope Jun 12, 2024
48c606c
fix props.refresh
LimberHope Jun 12, 2024
40beace
fix storybook
dottyy Jun 12, 2024
ef7b556
edit commentaryBox stories
dottyy Jun 12, 2024
d59ec36
fix spread error
LimberHope Jun 12, 2024
b023e57
improve code
pachonjcl Jun 13, 2024
27f74ec
update tests
pachonjcl Jun 13, 2024
3e069be
readd translations
pachonjcl Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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