diff --git a/backend/src/app/resolvers/site/sitePublic.resolver.spec.ts b/backend/src/app/resolvers/site/sitePublic.resolver.spec.ts index 1f46d7ec..6ab6b222 100644 --- a/backend/src/app/resolvers/site/sitePublic.resolver.spec.ts +++ b/backend/src/app/resolvers/site/sitePublic.resolver.spec.ts @@ -115,6 +115,7 @@ describe('SiteResolver', () => { undefined, undefined, undefined, + undefined, ); }); @@ -161,6 +162,7 @@ describe('SiteResolver', () => { undefined, undefined, undefined, + undefined, ); expect(result).toEqual(expectedFilteredSites); }); @@ -225,6 +227,7 @@ describe('SiteResolver', () => { undefined, undefined, undefined, + undefined, ); expect(result).toEqual(expectedFilteredSites); }); diff --git a/backend/src/app/resolvers/site/sitePublic.resolver.ts b/backend/src/app/resolvers/site/sitePublic.resolver.ts index ec4b71e1..92adc3d1 100644 --- a/backend/src/app/resolvers/site/sitePublic.resolver.ts +++ b/backend/src/app/resolvers/site/sitePublic.resolver.ts @@ -1,4 +1,4 @@ -import { Args, Query, Resolver } from '@nestjs/graphql'; +import { Args, Int, Query, Resolver } from '@nestjs/graphql'; import { Unprotected } from 'nest-keycloak-connect'; import { FetchSiteDetail, @@ -38,8 +38,8 @@ export class SitePublicResolver { @Query(() => SearchSiteResponse, { name: 'searchSites' }) async searchSites( @Args('searchParam', { type: () => String }) searchParam: string, - @Args('page', { type: () => String }) page: number, - @Args('pageSize', { type: () => String }) pageSize: number, + @Args('page', { type: () => Int }) page: number, + @Args('pageSize', { type: () => Int }) pageSize: number, @Args('id', { type: () => String, nullable: true }) id?: string, @Args('srStatus', { type: () => String, nullable: true }) srStatus?: string, @Args('siteRiskCode', { type: () => String, nullable: true }) @@ -71,6 +71,13 @@ export class SitePublicResolver { whenCreated?: Date, @Args('whenUpdated', { type: () => String, nullable: true }) whenUpdated?: Date, + @Args('siteIds', { + type: () => [String], + nullable: true, + description: + 'If provided, only applies the filters to the specified sites', + }) + siteIds?: string[], ) { this.sitesLogger.log('SiteResolver.searchSites() start '); return await this.siteService.searchSites( @@ -95,6 +102,7 @@ export class SitePublicResolver { longSeconds, whenCreated, whenUpdated, + siteIds, ); } diff --git a/backend/src/app/services/site/site.service.ts b/backend/src/app/services/site/site.service.ts index 9e3ddfaf..95896628 100644 --- a/backend/src/app/services/site/site.service.ts +++ b/backend/src/app/services/site/site.service.ts @@ -124,13 +124,20 @@ export class SiteService { longSeconds?: string, whenCreated?: Date, whenUpdated?: Date, + siteIds?: string[], ) { this.sitesLogger.log('SiteService.searchSites() start'); this.sitesLogger.debug('SiteService.searchSites() start'); const siteUtil: SiteUtil = new SiteUtil(); const response = new SearchSiteResponse(); - const query = this.siteRepository.createQueryBuilder('sites').where( + const query = this.siteRepository.createQueryBuilder('sites'); + + if (siteIds?.length) { + query.whereInIds(siteIds); + } + + query.andWhere( new Brackets((qb) => { qb.where('CAST(sites.id AS TEXT) LIKE :searchParam', { searchParam: `%${searchParam}%`, diff --git a/charts/app/templates/backend/templates/deployment.yaml b/charts/app/templates/backend/templates/deployment.yaml index cbc7ae0a..480f0767 100644 --- a/charts/app/templates/backend/templates/deployment.yaml +++ b/charts/app/templates/backend/templates/deployment.yaml @@ -68,9 +68,6 @@ spec: key: user name: postgres-crunchy-pguser-postgres resources: # this is optional - limits: - cpu: {{ .Values.backend.resources.limits.cpu }} - memory: {{ .Values.backend.resources.limits.memory }} requests: cpu: {{ .Values.backend.resources.requests.cpu }} memory: {{ .Values.backend.resources.requests.memory }} @@ -128,9 +125,6 @@ spec: - name: SEED_DATA_PATH value: "/mnt/sql/data_migration.sql" resources: # this is optional - limits: - cpu: {{ .Values.backend.resources.limits.cpu }} - memory: {{ .Values.backend.resources.limits.memory }} requests: cpu: {{ .Values.backend.resources.requests.cpu }} memory: {{ .Values.backend.resources.requests.memory }} @@ -217,9 +211,6 @@ spec: periodSeconds: 30 timeoutSeconds: 5 resources: # this is optional - limits: - cpu: {{ .Values.backend.resources.limits.cpu }} - memory: {{ .Values.backend.resources.limits.memory }} requests: cpu: {{ .Values.backend.resources.requests.cpu }} memory: {{ .Values.backend.resources.requests.memory }} diff --git a/charts/app/templates/frontend/templates/deployment.yaml b/charts/app/templates/frontend/templates/deployment.yaml index 3e8fdc37..b2164ec4 100644 --- a/charts/app/templates/frontend/templates/deployment.yaml +++ b/charts/app/templates/frontend/templates/deployment.yaml @@ -60,9 +60,6 @@ spec: periodSeconds: 30 timeoutSeconds: 5 resources: - limits: - cpu: 30m - memory: 75Mi requests: cpu: 15m memory: 25Mi diff --git a/charts/app/values-pr.yaml b/charts/app/values-pr.yaml index 957ad95c..391db7c4 100644 --- a/charts/app/values-pr.yaml +++ b/charts/app/values-pr.yaml @@ -23,9 +23,6 @@ backend: pdb: enabled: false resources: - limits: - cpu: 100m - memory: 250Mi requests: cpu: 50m memory: 150Mi diff --git a/charts/app/values.yaml b/charts/app/values.yaml index c609fc6d..467fa6be 100644 --- a/charts/app/values.yaml +++ b/charts/app/values.yaml @@ -45,9 +45,6 @@ backend: #-- the target cpu utilization percentage, is from request cpu and NOT LIMIT CPU. targetCPUUtilizationPercentage: 80 resources: - limits: - cpu: 150m - memory: 250Mi requests: cpu: 50m memory: 150Mi diff --git a/charts/crunchy/templates/PostgresCluster.yaml b/charts/crunchy/templates/PostgresCluster.yaml index 07e1e723..3472497c 100644 --- a/charts/crunchy/templates/PostgresCluster.yaml +++ b/charts/crunchy/templates/PostgresCluster.yaml @@ -54,9 +54,6 @@ spec: requests: cpu: {{ .Values.crunchy.pgmonitor.exporter.requests.cpu }} memory: {{ .Values.crunchy.pgmonitor.exporter.requests.memory }} - limits: - cpu: {{ .Values.crunchy.pgmonitor.exporter.limits.cpu }} - memory: {{ .Values.crunchy.pgmonitor.exporter.limits.memory }} {{ end }} @@ -71,9 +68,6 @@ spec: requests: cpu: {{ .Values.crunchy.instances.requests.cpu }} memory: {{ .Values.crunchy.instances.requests.memory }} - limits: - cpu: {{ .Values.crunchy.instances.limits.cpu }} - memory: {{ .Values.crunchy.instances.limits.memory }} sidecars: replicaCertCopy: @@ -81,9 +75,6 @@ spec: requests: cpu: {{ .Values.crunchy.instances.replicaCertCopy.requests.cpu }} memory: {{ .Values.crunchy.instances.replicaCertCopy.requests.memory }} - limits: - cpu: {{ .Values.crunchy.instances.replicaCertCopy.limits.cpu }} - memory: {{ .Values.crunchy.instances.replicaCertCopy.limits.memory }} dataVolumeClaimSpec: accessModes: - "ReadWriteOnce" @@ -177,9 +168,6 @@ spec: requests: cpu: {{ .Values.crunchy.pgBackRest.repoHost.requests.cpu }} memory: {{ .Values.crunchy.pgBackRest.repoHost.requests.memory }} - limits: - cpu: {{ .Values.crunchy.pgBackRest.repoHost.limits.cpu }} - memory: {{ .Values.crunchy.pgBackRest.repoHost.limits.memory }} sidecars: # this stuff is for the "pgbackrest" container in the "postgres-crunchy-ha" set of pods pgbackrest: @@ -187,25 +175,16 @@ spec: requests: cpu: {{ .Values.crunchy.pgBackRest.sidecars.pgBackRest.requests.cpu }} memory: {{ .Values.crunchy.pgBackRest.sidecars.pgBackRest.requests.memory }} - limits: - cpu: {{ .Values.crunchy.pgBackRest.sidecars.pgBackRest.limits.cpu }} - memory: {{ .Values.crunchy.pgBackRest.sidecars.pgBackRest.limits.memory }} pgbackrestConfig: resources: requests: cpu: {{ .Values.crunchy.pgBackRest.sidecars.requests.cpu }} memory: {{ .Values.crunchy.pgBackRest.sidecars.requests.memory }} - limits: - cpu: {{ .Values.crunchy.pgBackRest.sidecars.limits.cpu }} - memory: {{ .Values.crunchy.pgBackRest.sidecars.limits.memory }} jobs: resources: requests: cpu: {{ .Values.crunchy.pgBackRest.jobs.requests.cpu }} memory: {{ .Values.crunchy.pgBackRest.jobs.requests.memory }} - limits: - cpu: {{ .Values.crunchy.pgBackRest.jobs.limits.cpu }} - memory: {{ .Values.crunchy.pgBackRest.jobs.limits.memory }} {{- end }} patroni: dynamicConfiguration: @@ -238,9 +217,6 @@ spec: requests: cpu: {{ .Values.crunchy.proxy.pgBouncer.requests.cpu }} memory: {{ .Values.crunchy.proxy.pgBouncer.requests.memory }} - limits: - cpu: {{ .Values.crunchy.proxy.pgBouncer.limits.cpu }} - memory: {{ .Values.crunchy.proxy.pgBouncer.limits.memory }} affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: diff --git a/charts/crunchy/values.yaml b/charts/crunchy/values.yaml index 60a1dd91..2c50e09a 100644 --- a/charts/crunchy/values.yaml +++ b/charts/crunchy/values.yaml @@ -38,16 +38,10 @@ crunchy: # enable it for TEST and PROD, for PR based pipelines simply use single requests: cpu: 50m memory: 128Mi - limits: - cpu: 150m - memory: 256Mi replicaCertCopy: requests: cpu: 1m memory: 32Mi - limits: - cpu: 50m - memory: 64Mi pgBackRest: enabled: true @@ -80,37 +74,22 @@ crunchy: # enable it for TEST and PROD, for PR based pipelines simply use single requests: cpu: 5m memory: 32Mi - limits: - cpu: 20m - memory: 64Mi repoHost: requests: cpu: 20m memory: 128Mi - limits: - cpu: 50m - memory: 256Mi sidecars: requests: cpu: 5m memory: 16Mi - limits: - cpu: 20m - memory: 64Mi pgBackRest: # little more as these container in the statefulset pods do the pvc backups requests: cpu: 25m memory: 128Mi - limits: - cpu: 50m - memory: 256Mi jobs: requests: cpu: 20m memory: 256Mi - limits: - cpu: 100m - memory: 512Mi patroni: postgresql: @@ -135,9 +114,6 @@ crunchy: # enable it for TEST and PROD, for PR based pipelines simply use single requests: cpu: 5m memory: 32Mi - limits: - cpu: 20m - memory: 64Mi maxConnections: 10 # make sure less than postgres max connections # Postgres Cluster resource values: @@ -148,8 +124,5 @@ crunchy: # enable it for TEST and PROD, for PR based pipelines simply use single requests: cpu: 1m memory: 16Mi - limits: - cpu: 35m - memory: 32Mi databaseInitSQL: enabled: true diff --git a/frontend/src/app/components/button/Button.css b/frontend/src/app/components/button/Button.css index 5e979a9b..8196dcb6 100644 --- a/frontend/src/app/components/button/Button.css +++ b/frontend/src/app/components/button/Button.css @@ -75,3 +75,23 @@ .SITE-Button.tertiary:hover:not(:disabled) { background-color: var(--surface-tertiary-hover); } + +.SITE-Button.btn-icon { + background: transparent; + border: 1px solid transparent; + color: var(--surface-primary-default); +} +.SITE-Button.btn-icon:hover:not(:disabled) { + background-color: var(--surface-color-brand-blue-20); +} +.SITE-Button.btn-icon:focus { + background-color: var(--surface-color-brand-blue-20); + border: 1px solid var(--surface-color-primary-active-border); +} +.SITE-Button.btn-icon.btn-active { + background: var(--surface-color-primary-default); + color: var(--typography-color-primary-invert); +} +.SITE-Button.btn-icon.btn-active:hover { + background: var(--surface-color-primary-default); +} diff --git a/frontend/src/app/components/button/Button.tsx b/frontend/src/app/components/button/Button.tsx index eeaf5d8c..59403c98 100644 --- a/frontend/src/app/components/button/Button.tsx +++ b/frontend/src/app/components/button/Button.tsx @@ -3,20 +3,37 @@ import clsx from 'clsx'; import './Button.css'; -export type ButtonVariant = 'primary' | 'secondary' | 'tertiary'; +export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'icon'; export type ButtonSize = 'small' | 'medium'; type ButtonProps = ButtonHTMLAttributes & { size?: ButtonSize; variant?: ButtonVariant; + active?: boolean; }; export const Button = forwardRef( - ({ variant = 'primary', size = 'medium', className, ...props }, ref) => { + ( + { + variant = 'primary', + size = 'medium', + className, + active = false, + ...props + }, + ref, + ) => { + const variantCssClass = variant === 'icon' ? 'btn-icon' : variant; return ( ); }; @@ -100,10 +100,9 @@ const Pagination: React.FC = ({ {paginationDots}
{page(totalPagesRequired)}
- selectPage?.(currentPage + 1)} - /> +
); } else { @@ -129,10 +128,9 @@ const Pagination: React.FC = ({ if (startPage !== 1) { return ( - selectPage?.(currentPage - 1)} - /> +
{page(1)}
{paginationDots}
diff --git a/frontend/src/app/features/map/MapSearch.graphql b/frontend/src/app/features/map/MapSearch.graphql index 9c643bcb..29f6a66f 100644 --- a/frontend/src/app/features/map/MapSearch.graphql +++ b/frontend/src/app/features/map/MapSearch.graphql @@ -32,6 +32,38 @@ query MapSearch_findSiteBySiteId($siteId: String!) { } } +query MapSearch_filterSearchResults($siteIds: [String!], $page: Int!, $pageSize: Int!) { + searchSites(searchParam: "", page: $page, pageSize: $pageSize, siteIds: $siteIds) { + sites { + id + addrLine_1 + addrLine_2 + addrLine_3 + city + srStatus + siteRiskCode + generalDescription + commonName + latdeg + latDegrees + latMinutes + latSeconds + longdeg + longDegrees + longMinutes + longSeconds + latlongReliabilityFlag + whoCreated + whenCreated + whenCreated + consultantSubmitted + } + count + page + pageSize + } +} + mutation MapSearch_addCartItem($siteId: String!) { addCartItem(cartDTO: { siteId: $siteId, diff --git a/frontend/src/app/features/map/siteDrawer/SearchResultsDrawerContent.tsx b/frontend/src/app/features/map/siteDrawer/SearchResultsDrawerContent.tsx index 796f08d8..2cec0099 100644 --- a/frontend/src/app/features/map/siteDrawer/SearchResultsDrawerContent.tsx +++ b/frontend/src/app/features/map/siteDrawer/SearchResultsDrawerContent.tsx @@ -1,5 +1,24 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; import { Site } from '../MapView'; +import SearchResults from '../../site/searchResults/SearchResults'; +import { SearchResultsFilters } from '../../site/searchResults/SearchResultsFilters'; +import { SearchResultsActions } from '../../site/searchResults/SearchResultsActions'; +import FilterPills from '../../site/filters/FilterPills'; +import { getSiteSearchResultsColumns } from '../../site/dto/Columns'; +import { TableColumn } from '../../../components/table/TableColumn'; +import { + MapSearch_FilterSearchResultsQuery, + useMapSearch_FilterSearchResultsQuery, +} from '../../../../graphql/generated'; +import { SpinnerIcon } from '../../../components/common/icon'; +import useDebouncedValue from '../../../helpers/useDebouncedValue'; + +const defaultColumns = getSiteSearchResultsColumns(); + +type Pagination = { + page: number; + pageSize: number; +}; interface SearchResultsDrawerContentProps { sites: Site[]; @@ -7,11 +26,110 @@ interface SearchResultsDrawerContentProps { } export const SearchResultsDrawerContent: FC< SearchResultsDrawerContentProps -> = ({ sites, loading }) => { +> = ({ sites, loading: siteIdsLoading }) => { + const siteIds = sites.map((site) => site.id); + // Saving fetched data to local state allows us avoid table component flickering + // when a refetch with new variables takes place + const [searchResults, setSearchResults] = useState< + MapSearch_FilterSearchResultsQuery['searchSites'] + >({ + sites: [], + pageSize: 0, + count: 0, + page: 1, + }); + + const [pagination, setPagination] = useState({ + page: 1, + pageSize: 5, + }); + + const { loading: siteDetailsLoading } = useMapSearch_FilterSearchResultsQuery( + { + variables: { + siteIds, + page: pagination.page, + pageSize: pagination.pageSize, + }, + onCompleted: (data) => { + setSearchResults(data.searchSites); + }, + }, + ); + + const siteDetailsLoadingDebounced = useDebouncedValue(siteDetailsLoading); + + const [columnsToDisplay, setColumnsToDisplay] = + useState(defaultColumns); + const [filtersFormData, setFiltersFormData] = useState<{ + [key: string]: any | [Date, Date]; + }>({}); + const [selectedRows, setSelectedRows] = useState([]); + + const toggleColumnSelectionForDisplay = (column: TableColumn) => { + setColumnsToDisplay((prevColumns) => + prevColumns.map((item) => + item.id === column.id && !item.disabled + ? { ...item, isChecked: !item.isChecked } + : item, + ), + ); + }; + + const onFiltersChange = (key: string, value: any) => { + setFiltersFormData((prevData) => ({ + ...prevData, + [key]: value, + })); + }; + + const onFiltersSubmit = (e: React.FormEvent) => { + console.log('TODO'); + }; + + const changeHandler = (event: any) => { + if (!event) return; + const { property, value, row, selected: selectAllChecked } = event; + + if (property === 'select_row') { + setSelectedRows((prevRows) => + value + ? prevRows.some((r) => r.id === row.id) + ? prevRows + : [...prevRows, row] + : prevRows.filter((r) => r.id !== row.id), + ); + } else if (property === 'select_all') { + setSelectedRows(selectAllChecked ? [...value] : []); + } + }; + + const loading = siteIdsLoading || siteDetailsLoadingDebounced; + return ( -
- {loading &&
loading
} -
{JSON.stringify(sites, null, 2)}
+
+ setColumnsToDisplay(defaultColumns)} + filtersFormData={filtersFormData} + onFiltersChange={onFiltersChange} + onFiltersSubmit={onFiltersSubmit} + onFiltersReset={() => setFiltersFormData({})} + /> + + + console.log('todo')} /> + + {loading && } + + setPagination({ page, pageSize })} + data={searchResults.sites} + columns={columnsToDisplay.filter((x) => x.isChecked === true)} + totalRecords={searchResults.count} + changeHandler={changeHandler} + />
); }; diff --git a/frontend/src/app/features/site/Search.tsx b/frontend/src/app/features/site/Search.tsx index a77b7cae..fc296f08 100644 --- a/frontend/src/app/features/site/Search.tsx +++ b/frontend/src/app/features/site/Search.tsx @@ -17,39 +17,22 @@ import { currentPageSelection, currentPageSize, } from './dto/SiteSlice'; -import SearchResults from './SearchResults'; +import SearchResults from './searchResults/SearchResults'; import { - ShoppingCartIcon, - FileExportIcon, - TableColumnsIcon, - FilterIcon, CircleXMarkIcon, MagnifyingGlassIcon, - BarsIcon, } from '../../components/common/icon'; import Intro from './Intro'; -import Column from './columns/Column'; import { TableColumn } from '../../components/table/TableColumn'; import { getSiteSearchResultsColumns } from './dto/Columns'; -import SiteFilterForm from './filters/SiteFilterForm'; import PageContainer from '../../components/simple/PageContainer'; -import { - flattenFormRows, - formatDateRange, - getUser, - isUserOfType, - UserRoleType, -} from '../../helpers/utility'; -import { useAuth } from 'react-oidc-context'; -import { addCartItem, resetCartItemAddedStatus } from '../cart/CartSlice'; -import AddToFolio from '../folios/AddToFolio'; -import { downloadCSV } from '../../helpers/csvExport/csvExport'; +import { flattenFormRows, formatDateRange } from '../../helpers/utility'; import FilterPills from './filters/FilterPills'; import { formRows } from './dto/SiteFilterConfig'; -import { Button } from '../../components/button/Button'; +import { SearchResultsFilters } from './searchResults/SearchResultsFilters'; +import { SearchResultsActions } from './searchResults/SearchResultsActions'; const Search = () => { - const auth = useAuth(); const [searchText, setSearchText] = useState(''); const dispatch = useDispatch(); const sites = useSelector(selectAllSites); @@ -58,14 +41,11 @@ const Search = () => { const currentPageSizeInState = useSelector(currentPageSize); const totalRecords = useSelector(resultsCount); const [noUserAction, setUserAction] = useState(true); - const [displayColumn, SetDisplayColumns] = useState(false); - const [displayFilters, SetDisplayFilters] = useState(false); const columns = getSiteSearchResultsColumns(); const [columnsToDisplay, setColumnsToDisplay] = useState([ ...columns, ]); - const [showMobileTableMenu, SetShowMobileTableMenu] = useState(false); const [selectedRows, SetSelectedRows] = useState([]); const [selectedFilters, setSelectedFilters] = useState< { key: string; value: string; label: string }[] @@ -103,27 +83,10 @@ const Search = () => { } }, [currentPageSizeInState]); - const hideColumns = () => { - SetDisplayColumns(false); - }; - const resetDefaultColums = () => { setColumnsToDisplay(columns); }; - const cancelSearchFilter = () => { - SetDisplayFilters(false); - }; - - const search = (value: any) => { - return sites; - }; - - const dynamicSearchIconStyle = (left: any) => ({ - position: `absoulte`, - left: `${left}px`, - }); - const pageChange = (pageRequested: number, resultsCount: number) => { dispatch( updatePageSizeSetting({ @@ -177,35 +140,6 @@ const Search = () => { } }; - const customStyle: React.CSSProperties = { - left: - document - .getElementsByClassName('form-control textSearch')[0] - ?.getBoundingClientRect().x + - 2 + - 'px', - position: 'absolute', - color: 'grey', - margin: '4px', - }; - - const handleAddToShoppingCart = () => { - const loggedInUser = getUser(); - if (loggedInUser === null) { - auth.signinRedirect({ extraQueryParams: { kc_idp_hint: 'bceid' } }); - } else { - const cartItems = selectedRows.map((row) => { - return { - siteId: row.id, - price: 200.11, - }; - }); - - dispatch(resetCartItemAddedStatus(null)); - dispatch(addCartItem(cartItems)).unwrap(); - } - }; - const changeHandler = (event: any) => { if (event && event.property === 'select_row') { if (event.value) { @@ -246,12 +180,6 @@ const Search = () => { } }; - const handleExport = () => { - if (selectedRows.length > 0) { - downloadCSV(selectedRows); - } - }; - const handleInputChange = (key: string, value: any) => { setFormData((prevData) => ({ ...prevData, @@ -366,25 +294,6 @@ const Search = () => {
)} - {/* {!noUserAction ? null : ( - - - )} - - {noUserAction ? null : ( - - )} */} @@ -398,128 +307,16 @@ const Search = () => { className="row search-container results" aria-label="search-results-section-title" > -
-
-

Results

-
-
-
{ - SetDisplayColumns(!displayColumn); - SetDisplayFilters(false); - }} - > - - Columns -
-
{ - SetDisplayFilters(!displayFilters); - SetDisplayColumns(false); - }} - > - - Filters -
-
- -
- {displayFilters && ( - - )} - {displayColumn ? ( -
- {' '} - -
- ) : null} -
- {!isUserOfType(UserRoleType.INTERNAL) && ( - - )} - {!isUserOfType(UserRoleType.INTERNAL) && ( - row.id)} - /> - )} - - -
+ + {
x.isChecked === true)} totalRecords={totalRecords} changeHandler={changeHandler} diff --git a/frontend/src/app/features/site/SearchResults.css b/frontend/src/app/features/site/SearchResults.css deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/app/features/site/dto/Columns.ts b/frontend/src/app/features/site/dto/Columns.ts index 3fc8ada8..f727d1c0 100644 --- a/frontend/src/app/features/site/dto/Columns.ts +++ b/frontend/src/app/features/site/dto/Columns.ts @@ -26,7 +26,7 @@ const getSiteSearchResultsColumns = () => { isDefault: true, sortOrder: 1, isChecked: true, - displayType: getLinkColumnType('Site ID', 'id', '', 'site/details/'), + displayType: getLinkColumnType('Site ID', 'id', '', '/site/details/'), stickyCol: true, }, new TableColumn( @@ -259,7 +259,7 @@ const getSiteSearchResultsColumns = () => { 'Details', 'id', '', - 'site/details/', + '/site/details/', 'Details', ), linkRedirectionURL: 'site/details/', diff --git a/frontend/src/app/features/site/dto/SiteSlice.ts b/frontend/src/app/features/site/dto/SiteSlice.ts index 37c1113d..52b02470 100644 --- a/frontend/src/app/features/site/dto/SiteSlice.ts +++ b/frontend/src/app/features/site/dto/SiteSlice.ts @@ -66,26 +66,21 @@ export const fetchSites = createAsyncThunk( async ( args: { searchParam?: string; - page?: string; - pageSize?: string; + page?: number; + pageSize?: number; filter?: {}; }, { getState }, ) => { try { - const { - searchParam = '', - page = '1', - pageSize = '10', - filter = {}, - } = args; + const { searchParam = '', filter = {} } = args; const state: any = getState(); const response = await getAxiosInstance().post(GRAPHQL, { query: print(graphQlSiteQuery(filter)), variables: { searchParam: searchParam, - page: '' + state.sites.currentPage, - pageSize: '' + state.sites.pageSize, + page: state.sites.currentPage, + pageSize: state.sites.pageSize, ...filter, }, }); diff --git a/frontend/src/app/features/site/graphql/Site.ts b/frontend/src/app/features/site/graphql/Site.ts index 4d1b7138..855fec86 100644 --- a/frontend/src/app/features/site/graphql/Site.ts +++ b/frontend/src/app/features/site/graphql/Site.ts @@ -17,7 +17,7 @@ export const graphQlSiteQuery = (filter: {}) => { } return gql` -query searchSites($searchParam: String!, $page: String!, $pageSize: String!, ${fieldsArgString}){ +query searchSites($searchParam: String!, $page: Int!, $pageSize: Int!, ${fieldsArgString}){ searchSites(searchParam: $searchParam, , page: $page, pageSize: $pageSize, ${fieldsString}) { sites { diff --git a/frontend/src/app/features/site/SearchResults.test.jsx b/frontend/src/app/features/site/searchResults/SearchResults.test.tsx similarity index 81% rename from frontend/src/app/features/site/SearchResults.test.jsx rename to frontend/src/app/features/site/searchResults/SearchResults.test.tsx index 2339c9cd..5b8a4605 100644 --- a/frontend/src/app/features/site/SearchResults.test.jsx +++ b/frontend/src/app/features/site/searchResults/SearchResults.test.tsx @@ -4,14 +4,14 @@ import userEvent from '@testing-library/user-event'; import SearchResults from './SearchResults'; import { Provider } from 'react-redux'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import { RequestStatus } from '../../helpers/requests/status'; -import { getSiteSearchResultsColumns } from './dto/Columns'; +import configureStore, { MockStoreEnhanced } from 'redux-mock-store'; +import { RequestStatus } from '../../../helpers/requests/status'; +import { getSiteSearchResultsColumns } from '../dto/Columns'; const mockStore = configureStore([]); describe('SearchResults Component', () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore({ @@ -25,10 +25,16 @@ describe('SearchResults Component', () => { }); test('renders no results found when data is empty', () => { - const emptyData = []; + const emptyData: any[] = []; const { container } = render( - {}} /> + {}} + columns={[]} + totalRecords={0} + changeHandler={jest.fn} + /> , ); const noResultsText = screen.getByText('No Results Found'); @@ -54,6 +60,8 @@ describe('SearchResults Component', () => { data={mockData} pageChange={(currentPage, resultsPerPage) => {}} columns={getSiteSearchResultsColumns()} + totalRecords={0} + changeHandler={jest.fn} /> ), path: '/', @@ -86,8 +94,9 @@ describe('SearchResults Component', () => { {}} - pageChange={() => {}} + pageChange={jest.fn} + totalRecords={0} + changeHandler={jest.fn} /> ), path: '/', @@ -125,7 +134,9 @@ describe('SearchResults Component', () => { {}} + pageChange={jest.fn} + totalRecords={0} + changeHandler={jest.fn} /> ), path: '/', diff --git a/frontend/src/app/features/site/SearchResults.tsx b/frontend/src/app/features/site/searchResults/SearchResults.tsx similarity index 78% rename from frontend/src/app/features/site/SearchResults.tsx rename to frontend/src/app/features/site/searchResults/SearchResults.tsx index e3813900..abf01abe 100644 --- a/frontend/src/app/features/site/SearchResults.tsx +++ b/frontend/src/app/features/site/searchResults/SearchResults.tsx @@ -1,12 +1,11 @@ import React, { FC, useEffect, useState } from 'react'; -import { SpinnerIcon, SortIcon } from '../../components/common/icon'; -import './SearchResults.css'; -import { loadingState } from './dto/SiteSlice'; -import { RequestStatus } from '../../helpers/requests/status'; +import { SpinnerIcon, SortIcon } from '../../../components/common/icon'; +import { loadingState } from '../dto/SiteSlice'; +import { RequestStatus } from '../../../helpers/requests/status'; import { useSelector } from 'react-redux'; -import { TableColumn } from '../../components/table/TableColumn'; -import Pagination from '../../components/table/pagination/Pagination'; -import Table from '../../components/table/Table'; +import { TableColumn } from '../../../components/table/TableColumn'; +import Pagination from '../../../components/table/pagination/Pagination'; +import Table from '../../../components/table/Table'; interface ColumnProps { data: any; diff --git a/frontend/src/app/features/site/searchResults/SearchResultsActions.tsx b/frontend/src/app/features/site/searchResults/SearchResultsActions.tsx new file mode 100644 index 00000000..5088212e --- /dev/null +++ b/frontend/src/app/features/site/searchResults/SearchResultsActions.tsx @@ -0,0 +1,77 @@ +import { FC } from 'react'; +import { useAuth } from 'react-oidc-context'; +import { useDispatch } from 'react-redux'; + +import { AppDispatch } from '../../../Store'; +import { Button } from '../../../components/button/Button'; +import { getUser, isUserOfType, UserRoleType } from '../../../helpers/utility'; +import AddToFolio from '../../folios/AddToFolio'; +import { + FileExportIcon, + ShoppingCartIcon, +} from '../../../components/common/icon'; +import { downloadCSV } from '../../../helpers/csvExport/csvExport'; +import { addCartItem, resetCartItemAddedStatus } from '../../cart/CartSlice'; + +interface SearchResultsActionsProps { + selectedRows: any[]; // TODO: type this properly, should be Site +} +export const SearchResultsActions: FC = ({ + selectedRows, +}) => { + const auth = useAuth(); + const dispatch = useDispatch(); + + const handleExport = () => { + if (selectedRows.length > 0) { + downloadCSV(selectedRows); + } + }; + + const handleAddToShoppingCart = () => { + const loggedInUser = getUser(); + if (loggedInUser === null) { + auth.signinRedirect({ extraQueryParams: { kc_idp_hint: 'bceid' } }); + } else { + const cartItems = selectedRows.map((row) => { + return { + siteId: row.id, + price: 200.11, + }; + }); + + dispatch(resetCartItemAddedStatus(null)); + dispatch(addCartItem(cartItems)).unwrap(); + } + }; + + return ( +
+ {!isUserOfType(UserRoleType.INTERNAL) && ( + + )} + {!isUserOfType(UserRoleType.INTERNAL) && ( + row.id)} + /> + )} + + +
+ ); +}; diff --git a/frontend/src/app/features/site/searchResults/SearchResultsFilters.tsx b/frontend/src/app/features/site/searchResults/SearchResultsFilters.tsx new file mode 100644 index 00000000..ea6451c2 --- /dev/null +++ b/frontend/src/app/features/site/searchResults/SearchResultsFilters.tsx @@ -0,0 +1,112 @@ +import { FC, useState } from 'react'; +import { + BarsIcon, + FilterIcon, + TableColumnsIcon, +} from '../../../components/common/icon'; + +import { Dropdown } from 'react-bootstrap'; +import { Button } from '../../../components/button/Button'; +import Column from '../columns/Column'; +import type { TableColumn } from '../../../components/table/TableColumn'; +import SiteFilterForm from '../filters/SiteFilterForm'; + +interface SearchResultsFiltersProps { + columns: TableColumn[]; + onColumnSelectionChange: (column: TableColumn) => void; + resetColumns: () => void; + filtersFormData: { [key: string]: any | [Date, Date] }; + onFiltersChange: (key: string, value: any) => void; + onFiltersSubmit: (e: React.FormEvent) => void; + onFiltersReset: () => void; +} + +type PanelOption = 'filters' | 'columns' | null; +export const SearchResultsFilters: FC = ({ + columns, + onColumnSelectionChange, + resetColumns, + filtersFormData, + onFiltersChange, + onFiltersSubmit, + onFiltersReset, +}) => { + const [panelToShow, setPanelToShow] = useState(null); + + const togglePanel = (panel: PanelOption) => { + if (panelToShow === panel) { + setPanelToShow(null); + return; + } + setPanelToShow(panel); + }; + + return ( + <> +
+
+

Results

+
+ +
+
{ + togglePanel('columns'); + }} + > + + Columns +
+
{ + togglePanel('filters'); + }} + > + + Filters +
+
+ + + + + + togglePanel('columns')} + className="d-flex align-items-center gap-2" + > + + Columns + + togglePanel('filters')} + className="d-flex align-items-center gap-2" + > + + Filters + + + +
+ {panelToShow === 'columns' && ( + togglePanel(null)} + /> + )} + {panelToShow === 'filters' && ( + togglePanel(null)} + /> + )} + + ); +}; diff --git a/frontend/src/graphql/generated.ts b/frontend/src/graphql/generated.ts index 9e38bf2a..0409aeb9 100644 --- a/frontend/src/graphql/generated.ts +++ b/frontend/src/graphql/generated.ts @@ -809,9 +809,10 @@ export type QuerySearchSitesArgs = { longMinutes?: InputMaybe; longSeconds?: InputMaybe; longdeg?: InputMaybe; - page: Scalars['String']['input']; - pageSize: Scalars['String']['input']; + page: Scalars['Int']['input']; + pageSize: Scalars['Int']['input']; searchParam: Scalars['String']['input']; + siteIds?: InputMaybe>; siteRiskCode?: InputMaybe; srStatus?: InputMaybe; whenCreated?: InputMaybe; @@ -1351,6 +1352,15 @@ export type MapSearch_FindSiteBySiteIdQueryVariables = Exact<{ export type MapSearch_FindSiteBySiteIdQuery = { __typename?: 'Query', findSiteBySiteId: { __typename?: 'FetchSiteDetail', data?: { __typename?: 'Sites', id: string, addrLine_1: string, addrLine_2?: string | null, addrLine_3?: string | null, addrLine_4?: string | null, city: string, latdeg?: number | null, longdeg?: number | null, latDegrees?: number | null, latMinutes?: number | null, latSeconds?: number | null, longDegrees?: number | null, longMinutes?: number | null, longSeconds?: number | null, generalDescription?: string | null, siteRiskCode: string } | null } }; +export type MapSearch_FilterSearchResultsQueryVariables = Exact<{ + siteIds?: InputMaybe | Scalars['String']['input']>; + page: Scalars['Int']['input']; + pageSize: Scalars['Int']['input']; +}>; + + +export type MapSearch_FilterSearchResultsQuery = { __typename?: 'Query', searchSites: { __typename?: 'SearchSiteResponse', count: number, page: number, pageSize: number, sites: Array<{ __typename?: 'Sites', id: string, addrLine_1: string, addrLine_2?: string | null, addrLine_3?: string | null, city: string, srStatus: string, siteRiskCode: string, generalDescription?: string | null, commonName: string, latdeg?: number | null, latDegrees?: number | null, latMinutes?: number | null, latSeconds?: number | null, longdeg?: number | null, longDegrees?: number | null, longMinutes?: number | null, longSeconds?: number | null, latlongReliabilityFlag: string, whoCreated: string, whenCreated: any, consultantSubmitted?: string | null }> } }; + export type MapSearch_AddCartItemMutationVariables = Exact<{ siteId: Scalars['String']['input']; }>; @@ -1542,6 +1552,79 @@ export type MapSearch_FindSiteBySiteIdQueryHookResult = ReturnType; export type MapSearch_FindSiteBySiteIdSuspenseQueryHookResult = ReturnType; export type MapSearch_FindSiteBySiteIdQueryResult = Apollo.QueryResult; +export const MapSearch_FilterSearchResultsDocument = gql` + query MapSearch_filterSearchResults($siteIds: [String!], $page: Int!, $pageSize: Int!) { + searchSites( + searchParam: "" + page: $page + pageSize: $pageSize + siteIds: $siteIds + ) { + sites { + id + addrLine_1 + addrLine_2 + addrLine_3 + city + srStatus + siteRiskCode + generalDescription + commonName + latdeg + latDegrees + latMinutes + latSeconds + longdeg + longDegrees + longMinutes + longSeconds + latlongReliabilityFlag + whoCreated + whenCreated + whenCreated + consultantSubmitted + } + count + page + pageSize + } +} + `; + +/** + * __useMapSearch_FilterSearchResultsQuery__ + * + * To run a query within a React component, call `useMapSearch_FilterSearchResultsQuery` and pass it any options that fit your needs. + * When your component renders, `useMapSearch_FilterSearchResultsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useMapSearch_FilterSearchResultsQuery({ + * variables: { + * siteIds: // value for 'siteIds' + * page: // value for 'page' + * pageSize: // value for 'pageSize' + * }, + * }); + */ +export function useMapSearch_FilterSearchResultsQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: MapSearch_FilterSearchResultsQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(MapSearch_FilterSearchResultsDocument, options); + } +export function useMapSearch_FilterSearchResultsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(MapSearch_FilterSearchResultsDocument, options); + } +export function useMapSearch_FilterSearchResultsSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(MapSearch_FilterSearchResultsDocument, options); + } +export type MapSearch_FilterSearchResultsQueryHookResult = ReturnType; +export type MapSearch_FilterSearchResultsLazyQueryHookResult = ReturnType; +export type MapSearch_FilterSearchResultsSuspenseQueryHookResult = ReturnType; +export type MapSearch_FilterSearchResultsQueryResult = Apollo.QueryResult; export const MapSearch_AddCartItemDocument = gql` mutation MapSearch_addCartItem($siteId: String!) { addCartItem(cartDTO: {siteId: $siteId, price: 0}) { diff --git a/ora2pg/openshift/temp-pod.yaml b/ora2pg/openshift/temp-pod.yaml index 1d178964..e512d3c9 100644 --- a/ora2pg/openshift/temp-pod.yaml +++ b/ora2pg/openshift/temp-pod.yaml @@ -11,9 +11,6 @@ spec: - name: sql-volume mountPath: /mnt/sql resources: - limits: - cpu: "80m" - memory: "128Mi" requests: cpu: "40m" memory: "64Mi"