From 568810b54b9aba48a7fc2496420600b72c406f45 Mon Sep 17 00:00:00 2001 From: afwilcox Date: Thu, 12 Dec 2024 12:41:35 -0800 Subject: [PATCH 01/14] chore: update 0.6.9 main (#821) Co-authored-by: jon-funk Co-authored-by: Derek Roberts --- .github/workflows/pr-close.yml | 4 ++-- .github/workflows/release-main.yml | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-close.yml b/.github/workflows/pr-close.yml index 519709f4c..a0b95c61c 100644 --- a/.github/workflows/pr-close.yml +++ b/.github/workflows/pr-close.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v4 - run: ./.github/scripts/cleanup_pvcs.sh env: - OC_NAMESPACE: ${{ vars.OC_NAMESPACE }} + OC_NAMESPACE: ${{ secrets.OC_NAMESPACE }} OC_SERVER: ${{ vars.OC_SERVER }} OC_TOKEN: ${{ secrets.OC_TOKEN }} - PR_NUMBER: ${{ github.event.number }} \ No newline at end of file + PR_NUMBER: ${{ github.event.number }} diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 0a1ac027b..70d89a78a 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -71,7 +71,7 @@ jobs: uses: bcgov/quickstart-openshift-helpers/.github/workflows/.deployer.yml@v0.8.3 secrets: oc_namespace: ${{ secrets.OC_NAMESPACE }} - oc_token: ${{ secrets.OC_TOKEN }} + oc_token: ${{ secrets.OC_BEARER_TOKEN }} with: environment: prod tag: ${{ needs.vars.outputs.pr }} @@ -84,8 +84,6 @@ jobs: --set webeoc.pdb.enabled=true --set nats.config.cluster.replicas=3 --set nats.config.cluster.enabled=true - --set bitnami-pg.backup.cronjob.storage.size=512Mi - --set bitnami-pg.primary.persistence.size=512Mi --set backup.enabled=true --set backup.persistence.size=256Mi From f4d5997efe733d196a549e613f2c262877ab56a7 Mon Sep 17 00:00:00 2001 From: Ryan Rondeau Date: Thu, 12 Dec 2024 13:29:56 -0800 Subject: [PATCH 02/14] fix: Map Search Filters / Officers (#817) Co-authored-by: afwilcox --- backend/src/v1/complaint/complaint.service.ts | 49 ++++++++++--------- backend/src/v1/officer/officer.service.ts | 4 +- .../complaints/complaint-filter.tsx | 1 + ...plaint-map-with-server-side-clustering.tsx | 14 ++++-- .../mapping/complaint-summary-popup.tsx | 4 +- 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/backend/src/v1/complaint/complaint.service.ts b/backend/src/v1/complaint/complaint.service.ts index d3e36eef1..7216db94d 100644 --- a/backend/src/v1/complaint/complaint.service.ts +++ b/backend/src/v1/complaint/complaint.service.ts @@ -434,8 +434,28 @@ export class ComplaintService { query: string, token: string, ): Promise> { + let caseSearchData = []; + if (complaintType === "ERS") { + // Search CM for any case files that may match based on authorization id + const { data, errors } = await get(token, { + query: `{getCasesFilesBySearchString (searchString: "${query}") + { + leadIdentifier, + caseIdentifier + } + }`, + }); + + if (errors) { + this.logger.error("GraphQL errors:", errors); + throw new Error("GraphQL errors occurred"); + } + + caseSearchData = data.getCasesFilesBySearchString; + } + builder.andWhere( - new Brackets(async (qb) => { + new Brackets((qb) => { qb.orWhere("complaint.complaint_identifier ILIKE :query", { query: `%${query}%`, }); @@ -502,23 +522,6 @@ export class ComplaintService { switch (complaintType) { case "ERS": { - // Search CM for any case files that may match based on authorization id - const { data, errors } = await get(token, { - query: `{getCasesFilesBySearchString (searchString: "${query}") - { - leadIdentifier, - caseIdentifier - } - }`, - }); - - if (errors) { - this.logger.error("GraphQL errors:", errors); - throw new Error("GraphQL errors occurred"); - } - - const caseSearchData = data.getCasesFilesBySearchString; - if (caseSearchData.length > 0) { qb.orWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", { complaint_identifiers: caseSearchData.map((caseData) => caseData.leadIdentifier), @@ -1133,16 +1136,16 @@ export class ComplaintService { const includeCosOrganization: boolean = Boolean(query || filters.community || filters.zone || filters.region); let builder = this._generateMapQueryBuilder(complaintType, includeCosOrganization); - //-- apply search - if (query) { - builder = await this._applySearch(builder, complaintType, query, token); - } - //-- apply filters if used if (Object.keys(filters).length !== 0) { builder = this._applyFilters(builder, filters as ComplaintFilterParameters, complaintType); } + //-- apply search + if (query) { + builder = await this._applySearch(builder, complaintType, query, token); + } + //-- only return complaints for the agency the user is associated with const agency = hasCEEBRole ? "EPO" : (await this._getAgencyByUser()).agency_code; agency && builder.andWhere("complaint.owned_by_agency_code.agency_code = :agency", { agency }); diff --git a/backend/src/v1/officer/officer.service.ts b/backend/src/v1/officer/officer.service.ts index df0180621..b9ad26c4b 100644 --- a/backend/src/v1/officer/officer.service.ts +++ b/backend/src/v1/officer/officer.service.ts @@ -34,8 +34,8 @@ export class OfficerService { .leftJoinAndSelect("officer.office_guid", "office") .leftJoinAndSelect("officer.person_guid", "person") .leftJoinAndSelect("office.agency_code", "agency") - // This view is slow, no need to join and select for the properties that are mapped in this call - //.leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit") + // This view is slow :( + .leftJoinAndSelect("office.cos_geo_org_unit", "cos_geo_org_unit") .leftJoinAndSelect("office.agency_code", "agency_code") .orderBy("person.last_name", "ASC") .getMany(); diff --git a/frontend/src/app/components/containers/complaints/complaint-filter.tsx b/frontend/src/app/components/containers/complaints/complaint-filter.tsx index 722fe4f15..82de291e2 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter.tsx @@ -77,6 +77,7 @@ export const ComplaintFilter: FC = ({ type }) => { const setFilter = useCallback( (name: string, value?: Option | Date | null) => { let payload: ComplaintFilterPayload = { filter: name, value }; + dispatch(updateFilter(payload)); }, [dispatch], diff --git a/frontend/src/app/components/containers/complaints/complaint-map-with-server-side-clustering.tsx b/frontend/src/app/components/containers/complaints/complaint-map-with-server-side-clustering.tsx index 4e4a65de4..aa43457d6 100644 --- a/frontend/src/app/components/containers/complaints/complaint-map-with-server-side-clustering.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-map-with-server-side-clustering.tsx @@ -7,7 +7,7 @@ import { ComplaintRequestPayload } from "@/app/types/complaints/complaint-filter import LeafletMapWithServerSideClustering from "@components/mapping/leaflet-map-with-server-side-clustering"; import { generateApiParameters, get } from "@common/api"; import config from "@/config"; -import { setMappedComplaintsCount } from "@/app/store/reducers/complaints"; +import { setComplaint, setComplaintSearchParameters, setMappedComplaintsCount } from "@/app/store/reducers/complaints"; type Props = { type: string; @@ -17,8 +17,11 @@ type Props = { export const generateMapComplaintRequestPayload = ( complaintType: string, filters: ComplaintFilters, + searchQuery: string, ): ComplaintRequestPayload => { const { + sortColumn, + sortOrder, region, zone, community, @@ -37,8 +40,8 @@ export const generateMapComplaintRequestPayload = ( } = filters; let common = { - sortColumn: "", // sort or order has no bearing on map data - sortOrder: "", // sort or order has no bearing on map data + sortColumn: sortColumn, + sortOrder: sortOrder, regionCodeFilter: region, zoneCodeFilter: zone, areaCodeFilter: community, @@ -49,6 +52,7 @@ export const generateMapComplaintRequestPayload = ( actionTakenFilter: actionTaken, outcomeAnimalStartDateFilter: outcomeAnimalStartDate, outcomeAnimalEndDateFilter: outcomeAnimalEndDate, + query: searchQuery, }; switch (complaintType) { @@ -96,7 +100,9 @@ export const ComplaintMapWithServerSideClustering: FC = ({ type, searchQu }, ) => { setLoadingMapData(true); - let payload = generateMapComplaintRequestPayload(type, filters); + let payload = generateMapComplaintRequestPayload(type, filters, searchQuery); + dispatch(setComplaint(null)); + dispatch(setComplaintSearchParameters(payload)); let parms: any = { bbox: bbox ? `${bbox.west},${bbox.south},${bbox.east},${bbox.north}` : undefined, // If the bbox is not provided, return all complaint clusters diff --git a/frontend/src/app/components/mapping/complaint-summary-popup.tsx b/frontend/src/app/components/mapping/complaint-summary-popup.tsx index a36212939..db68f9558 100644 --- a/frontend/src/app/components/mapping/complaint-summary-popup.tsx +++ b/frontend/src/app/components/mapping/complaint-summary-popup.tsx @@ -5,6 +5,7 @@ import { ComplaintDetails } from "@apptypes/complaints/details/complaint-details import { applyStatusClass, formatDate } from "@common/methods"; import { Badge, Button } from "react-bootstrap"; import { Popup } from "react-leaflet"; +import { useNavigate } from "react-router-dom"; interface Props { complaint_identifier: string; @@ -12,6 +13,7 @@ interface Props { } export const ComplaintSummaryPopup: FC = ({ complaint_identifier, complaintType }) => { + const navigate = useNavigate(); const { officerAssigned, natureOfComplaint, species, violationType, loggedDate, status, girType } = useAppSelector( selectComplaintHeader(complaintType), ); @@ -77,7 +79,7 @@ export const ComplaintSummaryPopup: FC = ({ complaint_identifier, complai size="sm" className="comp-map-popup-details-btn" id="view-complaint-details-button-id" - href={`/complaint/${complaintType}/${complaint_identifier}`} + onClick={() => navigate(`/complaint/${complaintType}/${complaint_identifier}`)} > View Details From 2bbb8d0957a8a63b8db208494c73c6cd39c63ef2 Mon Sep 17 00:00:00 2001 From: Scarlett Date: Mon, 16 Dec 2024 14:10:02 -0800 Subject: [PATCH 03/14] feat: CE-501 admin page --- frontend/src/app/App.tsx | 2 +- .../admin-edit-user.tsx} | 2 +- .../user-management/admin-select-user.tsx | 147 +++++++ .../admin/user-management/index.tsx | 384 ++++++++++++++++++ frontend/src/assets/sass/user-management.scss | 27 ++ 5 files changed, 560 insertions(+), 2 deletions(-) rename frontend/src/app/components/containers/admin/{user-management.tsx => user-management/admin-edit-user.tsx} (99%) create mode 100644 frontend/src/app/components/containers/admin/user-management/admin-select-user.tsx create mode 100644 frontend/src/app/components/containers/admin/user-management/index.tsx create mode 100644 frontend/src/assets/sass/user-management.scss diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index b7bd324ed..04ca489cd 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -16,7 +16,7 @@ import { ComplaintsWrapper } from "./components/containers/complaints/complaints import COMPLAINT_TYPES from "./types/app/complaint-types"; import { getCodeTableVersion, getConfigurations, getFeatureFlag, getOfficerDefaultZone } from "./store/reducers/app"; import { CreateComplaint } from "./components/containers/complaints/details/complaint-details-create"; -import { UserManagement } from "./components/containers/admin/user-management"; +import { UserManagement } from "@components/containers/admin/user-management"; import UserService from "./service/user-service"; import GenericErrorBoundary from "./components/error-handling/generic-error-boundary"; import { VerifyAccess } from "./components/containers/pages/verify-access"; diff --git a/frontend/src/app/components/containers/admin/user-management.tsx b/frontend/src/app/components/containers/admin/user-management/admin-edit-user.tsx similarity index 99% rename from frontend/src/app/components/containers/admin/user-management.tsx rename to frontend/src/app/components/containers/admin/user-management/admin-edit-user.tsx index ec5786333..2cea5bb17 100644 --- a/frontend/src/app/components/containers/admin/user-management.tsx +++ b/frontend/src/app/components/containers/admin/user-management/admin-edit-user.tsx @@ -16,7 +16,7 @@ import { Officer } from "@apptypes/person/person"; import { UUID } from "crypto"; import { ValidationMultiSelect } from "@common/validation-multiselect"; -export const UserManagement: FC = () => { +export const AdminEditUser: FC = () => { const dispatch = useAppDispatch(); const officers = useAppSelector(selectOfficersDropdown(true)); const officeAssignments = useAppSelector(selectOfficesForAssignmentDropdown); diff --git a/frontend/src/app/components/containers/admin/user-management/admin-select-user.tsx b/frontend/src/app/components/containers/admin/user-management/admin-select-user.tsx new file mode 100644 index 000000000..fc97e166a --- /dev/null +++ b/frontend/src/app/components/containers/admin/user-management/admin-select-user.tsx @@ -0,0 +1,147 @@ +// @ts-nocheck +import { FC, useEffect, useState } from "react"; +import { Button } from "react-bootstrap"; +import { useAppDispatch, useAppSelector } from "@hooks/hooks"; +import { assignOfficerToOffice, selectOfficersDropdown } from "@store/reducers/officer"; +import { CompSelect } from "@components/common/comp-select"; +import Option from "@apptypes/app/option"; +import { fetchOfficeAssignments, selectOfficesForAssignmentDropdown, selectOffices } from "@store/reducers/office"; +import { ToastContainer } from "react-toastify"; +import { ToggleError, ToggleSuccess } from "@common/toast"; +import { clearNotification, selectNotification } from "@store/reducers/app"; +import { selectAgencyDropdown, selectTeamDropdown } from "@store/reducers/code-table"; +import { CEEB_ROLE_OPTIONS } from "@constants/ceeb-roles"; +import { generateApiParameters, get, patch } from "@common/api"; +import config from "@/config"; +import { Officer } from "@apptypes/person/person"; +import { UUID } from "crypto"; +import { ValidationMultiSelect } from "@common/validation-multiselect"; +import "@assets/sass/user-management.scss"; + +interface AdminSelectUserProps { + // equipment: EquipmentDetailsDto; + // isEditDisabled: boolean; + // onEdit: (guid: string) => void; +} +export const AdminSelectUser: FC = ({ + setOfficer, + setOfficerError, + getUserIdir, + handleAddNewUser, + officers, + officer, + officerError, + userIdirs, + setSelectedUserIdir, + updateUserIdirByOfficerId, + officerGuid, + handleSubmit, + handleCancel, +}) => { + const handleOfficerChange = async (input: any) => { + setOfficerError(""); + if (input.value) { + setOfficer(input); + const lastName = input.label.split(",")[0]; + const firstName = input.label.split(",")[1].trim(); + await getUserIdir(input.value, lastName, firstName); + } + }; + + return ( + <> + +
+
+
+

User Administration

+ +
+ +

+ After selecting a user, click Edit for more options, such as: choosing an agency, + team/office, specifying roles, updating the last name and/or email address, temporarily disabling or + deleting the user. +

+
+
+
+
+
+
Select User
+
+ handleOfficerChange(evt)} + classNames={{ + menu: () => "top-layer-select", + }} + options={officers} + placeholder="Select" + enableValidation={true} + value={officer} + errorMessage={officerError} + /> +
+
+
+ {userIdirs.length >= 2 && ( + <> +
+
+
+

{`Found ${userIdirs.length} users with same name. Please select the correct email: `}

+ {userIdirs && + userIdirs.map((item, index) => { + return ( +
+ { + setSelectedUserIdir(item.username); + await updateUserIdirByOfficerId(item.username.split("@")[0], officerGuid); + }} + /> + +
+ ); + })} +
+
+ + )} +
+ {" "} +   + +
+
+
+
+ + ); +}; diff --git a/frontend/src/app/components/containers/admin/user-management/index.tsx b/frontend/src/app/components/containers/admin/user-management/index.tsx new file mode 100644 index 000000000..40738afe5 --- /dev/null +++ b/frontend/src/app/components/containers/admin/user-management/index.tsx @@ -0,0 +1,384 @@ +import { FC, useEffect, useState } from "react"; +import { Button } from "react-bootstrap"; +import { useAppDispatch, useAppSelector } from "@hooks/hooks"; +import { assignOfficerToOffice, selectOfficersDropdown } from "@store/reducers/officer"; +import { CompSelect } from "@components/common/comp-select"; +import Option from "@apptypes/app/option"; +import { fetchOfficeAssignments, selectOfficesForAssignmentDropdown, selectOffices } from "@store/reducers/office"; +import { ToastContainer } from "react-toastify"; +import { ToggleError, ToggleSuccess } from "@common/toast"; +import { clearNotification, selectNotification } from "@store/reducers/app"; +import { selectAgencyDropdown, selectTeamDropdown } from "@store/reducers/code-table"; +import { CEEB_ROLE_OPTIONS } from "@constants/ceeb-roles"; +import { generateApiParameters, get, patch } from "@common/api"; +import config from "@/config"; +import { Officer } from "@apptypes/person/person"; +import { UUID } from "crypto"; +import { ValidationMultiSelect } from "@common/validation-multiselect"; +import "@assets/sass/user-management.scss"; + +export const UserManagement: FC = () => { + const dispatch = useAppDispatch(); + const officers = useAppSelector(selectOfficersDropdown(true)); + const officeAssignments = useAppSelector(selectOfficesForAssignmentDropdown); + const notification = useAppSelector(selectNotification); + const teams = useAppSelector(selectTeamDropdown); + const agency = useAppSelector(selectAgencyDropdown); + + const availableOffices = useAppSelector(selectOffices); + + const [officer, setOfficer] = useState