Skip to content

[MDS-5758] Dam History #3463

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

Merged
merged 14 commits into from
Mar 18, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the corresponding history table definition for the {table} table
CREATE TABLE dam_version (
create_user VARCHAR(60),
create_timestamp TIMESTAMP WITHOUT TIME ZONE,
update_user VARCHAR(60),
update_timestamp TIMESTAMP WITHOUT TIME ZONE,
deleted_ind BOOLEAN,
dam_guid UUID NOT NULL,
mine_tailings_storage_facility_guid UUID,
dam_type dam_type,
dam_name VARCHAR(200),
latitude NUMERIC(9, 7),
longitude NUMERIC(11, 7),
operating_status operating_status,
consequence_classification consequence_classification,
permitted_dam_crest_elevation NUMERIC(10, 2),
current_dam_height NUMERIC(10, 2),
current_elevation NUMERIC(10, 2),
max_pond_elevation NUMERIC(10, 2),
min_freeboard_required NUMERIC(10, 2),
transaction_id BIGINT NOT NULL,
end_transaction_id BIGINT,
operation_type SMALLINT NOT NULL,
PRIMARY KEY (dam_guid, transaction_id)
);
CREATE INDEX ix_dam_version_operation_type ON dam_version (operation_type);
CREATE INDEX ix_dam_version_transaction_id ON dam_version (transaction_id);
CREATE INDEX ix_dam_version_end_transaction_id ON dam_version (end_transaction_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- This file was generated by the generate_history_table_ddl command
-- The file contains the data migration to backfill history records for the {table} table
with transaction AS (insert into transaction(id) values(DEFAULT) RETURNING id)
insert into dam_version (transaction_id, operation_type, end_transaction_id, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "dam_guid", "mine_tailings_storage_facility_guid", "dam_type", "dam_name", "latitude", "longitude", "operating_status", "consequence_classification", "permitted_dam_crest_elevation", "current_dam_height", "current_elevation", "max_pond_elevation", "min_freeboard_required")
select t.id, '0', null, "create_user", "create_timestamp", "update_user", "update_timestamp", "deleted_ind", "dam_guid", "mine_tailings_storage_facility_guid", "dam_type", "dam_name", "latitude", "longitude", "operating_status", "consequence_classification", "permitted_dam_crest_elevation", "current_dam_height", "current_elevation", "max_pond_elevation", "min_freeboard_required"
from dam,transaction t;
12 changes: 10 additions & 2 deletions services/common/src/components/history/DiffColumn.interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ export interface IDiffColumn {

export interface IDiffEntry {
updated_by: string;
updated_at: Date;
updated_at: string;
changeset: IDiffColumn[];
}

/**
* Used to map the diff titles and values to a more user-friendly format
* Supply *one of* hash, tranform, data
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autobots, TRANFORM! 😆

*
* Example structure:
* {
Expand All @@ -21,13 +22,20 @@ export interface IDiffEntry {
* { value: "above_ground", label: "Above Ground" },
* { value: "below_ground", label: "Underground" }
* ]
* hash: {
* above_ground: "Above Ground"
* below_ground: "Underground"
* }
* transform: (value) => formatDate(value)
* },
* }
*/
export interface DiffColumnValueMapper {
[key: string]: {
title: string;
title?: string;
data?: { value: string; label: string }[];
hash?: { [key: string]: string };
transform?: (original: string) => string;
};
}

Expand Down
38 changes: 28 additions & 10 deletions services/common/src/components/history/DiffColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
import React from "react";
import { Typography } from "antd";
import { formatSnakeCaseToSentenceCase } from "@mds/common/redux/utils/helpers";
import { DiffColumnProps } from "./DiffColumn.interface";
import { DiffColumnProps, IDiffColumn } from "./DiffColumn.interface";

const DiffColumn: React.FC<DiffColumnProps> = ({ differences, valueMapper }) => {
const valueOrNoData = (value: any) => {
if (typeof value === "boolean") {
return value ? "True" : "False";
}
return value ? value : "No Data";
return value ?? "No Data";
};


/**
* Maps the diff titles and values to a more user-friendly format based on the given valueMapper.
* If a valueMapper is not provided or a the given field does not have a corresponding mapping,
* the field_name will be formatted to sentence case for easier readability.
*/
const mappedDifferences = differences
.filter((change) => !change.field_name?.endsWith("_guid")) // Ignore any fields we can reasonably assume the user doesn't care about
.map((change) => {
const applyValueMapper = (diffs: IDiffColumn[]) => {
const filtered = diffs.filter((change) => !change.field_name?.endsWith("_guid"));
return filtered.map((change) => {
const mapper = valueMapper ? valueMapper[change.field_name] : null;
const fromMapped = mapper?.data?.find((data) => data.value === change.from);
const toMapped = mapper?.data?.find((data) => data.value === change.to);
let { from, to } = change;

if (mapper) {
if (mapper.hash) {
from = mapper.hash[from];
to = mapper.hash[to];
} else if (mapper.data) {
const fromMap = mapper.data.find((data) => data.value === change.from);
const toMap = mapper.data.find((data) => data.value === change.to);
from = fromMap?.label || from;
to = toMap?.label || to;
} else if (mapper.transform) {
from = mapper.transform(from);
to = mapper.transform(to);
}
}

return {
field_name: mapper?.title ?? formatSnakeCaseToSentenceCase(change.field_name, "_"),
from: fromMapped?.label || change.from,
to: toMapped?.label || change.to,
from,
to,
};
});
})
};

const mappedDifferences = applyValueMapper(differences);

return (
<div className="padding-md--top">
Expand Down
40 changes: 40 additions & 0 deletions services/common/src/components/history/DiffTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { FC } from "react";
import { DiffColumnValueMapper, IDiffColumn, IDiffEntry } from "./DiffColumn.interface";
import CoreTable from "../common/CoreTable";
import DiffColumn from "./DiffColumn";
import { renderDateColumn } from "../common/CoreTableCommonColumns";
import { ColumnsType } from "antd/lib/table";

interface DiffTableProps {
history: IDiffEntry[];
loading?: boolean;
valueMapper?: DiffColumnValueMapper;
columns?: ColumnsType;
}

const DiffTable: FC<DiffTableProps> = ({ history, valueMapper, columns = [], loading = false }) => {

const commonColumns = [
{
title: "Updated by",
dataIndex: "updated_by"
},
{ ...renderDateColumn("updated_at", "Date", true), defaultSortOrder: "descend" },
{
title: "Changes",
dataIndex: "changeset",
render: (differences: IDiffColumn[]) => <DiffColumn differences={differences} valueMapper={valueMapper} />
}
];

return <CoreTable
className="diff-table"
rowClassName="diff-table-row"
rowKey="updated_at"
columns={[...columns, ...commonColumns]}
dataSource={history ?? []}
loading={loading}
/>
};

export default DiffTable;
3 changes: 1 addition & 2 deletions services/common/src/components/tailings/AssociatedDams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import PlusCircleFilled from "@ant-design/icons/PlusCircleFilled";
import React, { FC } from "react";
import { getTsf } from "@mds/common/redux/reducers/tailingsReducer";
import moment from "moment";
import { storeDam } from "@mds/common/redux/actions/damActions";
import { storeDam } from "@mds/common/redux/slices/damSlice";
import { useHistory } from "react-router-dom";
import { IDam, ITailingsStorageFacility } from "@mds/common/interfaces";
import { ColumnsType } from "antd/lib/table";
Expand Down Expand Up @@ -43,7 +43,6 @@ const AssociatedDams: FC<AssociatedDamsProps> = (props) => {
};

const handleNavigateToCreate = () => {
dispatch(storeDam({}));
const url = GLOBAL_ROUTES?.ADD_DAM.dynamicRoute(tsf.mine_guid, tsf.mine_tailings_storage_facility_guid);
history.push(url);
};
Expand Down
26 changes: 26 additions & 0 deletions services/common/src/components/tailings/dam/DamDiffModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { render } from "@testing-library/react";
import { ReduxWrapper } from "@mds/common/tests/utils/ReduxWrapper";
import { DAM_WITH_HISTORY } from "@mds/common/tests/mocks/dataMocks";
import { damReducerType } from "@mds/common/redux/slices/damSlice";
import DamDiffModal from "./DamDiffModal";


const initialState = {
[damReducerType]: {
dams: {
[DAM_WITH_HISTORY.dam_guid]: DAM_WITH_HISTORY
}
}
};

describe("Dam History Modal", () => {
it("renders properly", () => {
const { container } = render(
<ReduxWrapper initialState={initialState}>
<DamDiffModal damGuid={DAM_WITH_HISTORY.dam_guid} />
</ReduxWrapper>
);
expect(container).toMatchSnapshot();
});
});
50 changes: 50 additions & 0 deletions services/common/src/components/tailings/dam/DamDiffModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { FC, useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "@mds/common/redux/rootState";
import { fetchDamHistory, getDamByGuid } from "@mds/common/redux/slices/damSlice";
import DiffTable from "../../history/DiffTable";
import { CONSEQUENCE_CLASSIFICATION_CODE_HASH, DAM_OPERATING_STATUS_HASH, DAM_TYPES_HASH } from "@mds/common/constants/strings";
import { formatDateTimeUserTz } from "@mds/common/redux/utils/helpers";
import { Typography } from "antd";

interface DamDiffModalProps {
damGuid: string;
}

const DamDiffModal: FC<DamDiffModalProps> = ({ damGuid }) => {
const dispatch = useAppDispatch();
const dam = useAppSelector(getDamByGuid(damGuid));
const [loading, setLoading] = useState(false);

const fetchData = () => {
setLoading(true);
dispatch(fetchDamHistory(damGuid)).then(() => {
setLoading(false);
});
};

useEffect(() => {
if (!dam?.history && !loading) {
fetchData();
}
}, [dam?.history]);

const dateTransform = { transform: (dateString: string) => formatDateTimeUserTz(dateString) || null };

const valueMapper = {
dam_type: { hash: DAM_TYPES_HASH },
operating_status: { hash: DAM_OPERATING_STATUS_HASH },
consequence_classification: { hash: CONSEQUENCE_CLASSIFICATION_CODE_HASH },
create_timestamp: dateTransform,
update_timestamp: dateTransform,
};

return <>
<Typography.Title level={3}>View History</Typography.Title>
<Typography.Paragraph>
You are viewing the past history of <b>{dam.dam_name}</b>
</Typography.Paragraph>
<DiffTable history={dam?.history} loading={loading} valueMapper={valueMapper} />
</>
};

export default DamDiffModal;
40 changes: 39 additions & 1 deletion services/common/src/components/tailings/dam/DamForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
DAM_OPERATING_STATUS,
DAM_TYPES,
} from "@mds/common/constants/strings";
import { Col, Popconfirm, Row, Typography } from "antd";
import { Alert, Button, Col, Popconfirm, Row, Typography } from "antd";
import {
decimalPlaces,
lat,
Expand All @@ -20,6 +20,10 @@ import React, { FC } from "react";
import { ITailingsStorageFacility, IDam } from "@mds/common/interfaces";
import RenderSelect from "../../forms/RenderSelect";
import RenderField from "../../forms/RenderField";
import { formatDateTime } from "@mds/common/redux/utils/helpers";
import { useAppDispatch } from "@mds/common/redux/rootState";
import { openModal } from "@mds/common/redux/actions/modalActions";
import DamDiffModal from "./DamDiffModal";

interface DamFormProps {
tsf: ITailingsStorageFacility;
Expand All @@ -36,6 +40,7 @@ interface Params {

const DamForm: FC<DamFormProps> = (props) => {
const { tsf, dam, canEditTSF, isEditMode, canEditDam } = props;
const dispatch = useAppDispatch();
const history = useHistory();
const { tailingsStorageFacilityGuid, mineGuid } = useParams<Params>();
const canEditTSFAndEditMode = canEditTSF && canEditDam;
Expand All @@ -50,6 +55,16 @@ const DamForm: FC<DamFormProps> = (props) => {
history.push(returnUrl);
};

const openDiffModal = () => {
dispatch(openModal({
props: {
title: "Dam History",
damGuid: dam.dam_guid,
},
content: DamDiffModal
}))
};

return (
<div>
<div className="margin-large--bottom">
Expand All @@ -68,6 +83,29 @@ const DamForm: FC<DamFormProps> = (props) => {
</Typography.Link>
</Popconfirm>
</div>
{dam?.update_timestamp && (
<Row>
<Col span={24}>
<Typography.Paragraph>
<Alert
description={`Last Updated by ${dam.update_user} on ${formatDateTime(
dam.update_timestamp
)}`}
showIcon
message=""
className="ant-alert-grey bullet"
type="info"
style={{ alignItems: "center" }}
action={
<Button className="margin-large--left" onClick={openDiffModal}>
View History
</Button>
}
/>
</Typography.Paragraph>
</Col>
</Row>
)}

<Field
id="dam_type"
Expand Down
Loading
Loading