diff --git a/app/app/api/private-cloud/products/_operations/search.ts b/app/app/api/private-cloud/products/_operations/search.ts
deleted file mode 100644
index 1ae152f6e..000000000
--- a/app/app/api/private-cloud/products/_operations/search.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ProjectStatus, Ministry, Cluster, Prisma } from '@prisma/client';
-import { Session } from 'next-auth';
-import { parsePaginationParams } from '@/helpers/pagination';
-import { searchPrivateCloudProducts } from '@/queries/private-cloud-products';
-
-export default async function searchOp({
- session,
- search,
- page,
- pageSize,
- ministry,
- cluster,
- status,
- sortKey,
- sortOrder,
- isTest,
-}: {
- session: Session;
- search: string;
- page: number;
- pageSize: number;
- ministry?: Ministry;
- cluster?: Cluster;
- status?: ProjectStatus;
- sortKey?: string;
- sortOrder?: Prisma.SortOrder;
- isTest: boolean;
-}) {
- const { skip, take } = parsePaginationParams(page, pageSize, 10);
-
- const { docs, totalCount } = await searchPrivateCloudProducts({
- session: session as Session,
- skip,
- take,
- ministry,
- cluster,
- status,
- search,
- sortKey,
- sortOrder,
- isTest,
- });
-
- return { docs, totalCount };
-}
diff --git a/app/app/api/private-cloud/products/download/route.test.ts b/app/app/api/private-cloud/products/download/route.test.ts
index 317d68dd6..96a888455 100644
--- a/app/app/api/private-cloud/products/download/route.test.ts
+++ b/app/app/api/private-cloud/products/download/route.test.ts
@@ -1,5 +1,5 @@
import { expect } from '@jest/globals';
-import { DecisionStatus, Ministry, Cluster } from '@prisma/client';
+import { DecisionStatus, Ministry, Cluster, ProjectStatus } from '@prisma/client';
import { parse } from 'csv-parse/sync';
import prisma from '@/core/prisma';
import { createSamplePrivateCloudProductData } from '@/helpers/mock-resources';
@@ -289,9 +289,9 @@ describe('Download Private Cloud Products - Validations', () => {
await mockSessionByRole('admin');
const res1 = await downloadPrivateCloudProjects({
- ministry: Ministry.AEST,
- cluster: Cluster.CLAB,
- includeInactive: false,
+ ministries: [Ministry.AEST],
+ clusters: [Cluster.CLAB],
+ status: [ProjectStatus.ACTIVE],
});
expect(res1.status).toBe(200);
@@ -331,7 +331,7 @@ describe('Download Private Cloud Products - Validations', () => {
await mockSessionByRole('admin');
const res1 = await downloadPrivateCloudProjects({
- cluster: 'INVALID' as Cluster,
+ clusters: ['INVALID' as Cluster],
});
expect(res1.status).toBe(400);
@@ -341,7 +341,7 @@ describe('Download Private Cloud Products - Validations', () => {
await mockSessionByRole('admin');
const res1 = await downloadPrivateCloudProjects({
- ministry: 'INVALID' as Ministry,
+ ministries: ['INVALID' as Ministry],
});
expect(res1.status).toBe(400);
diff --git a/app/app/api/private-cloud/products/download/route.ts b/app/app/api/private-cloud/products/download/route.ts
index 11fcdcf33..a44f3a325 100644
--- a/app/app/api/private-cloud/products/download/route.ts
+++ b/app/app/api/private-cloud/products/download/route.ts
@@ -4,30 +4,25 @@ import { NoContent, CsvResponse } from '@/core/responses';
import { ministryKeyToName, getTotalQuotaStr } from '@/helpers/product';
import { formatFullName } from '@/helpers/user';
import { createEvent } from '@/mutations/events';
+import { searchPrivateCloudProducts } from '@/queries/private-cloud-products';
import { PrivateProductCsvRecord } from '@/types/csv';
import { formatDateSimple } from '@/utils/date';
import { privateCloudProductSearchNoPaginationBodySchema } from '@/validation-schemas/private-cloud';
-import searchOp from '../_operations/search';
export const POST = createApiHandler({
roles: ['user'],
validations: { body: privateCloudProductSearchNoPaginationBodySchema },
})(async ({ session, body }) => {
- const { search = '', ministry, cluster, includeInactive = false, showTest = false, sortKey, sortOrder } = body;
-
const searchProps = {
- search,
page: 1,
pageSize: 10000,
- ministry,
- cluster,
- status: includeInactive ? undefined : ProjectStatus.ACTIVE,
- sortKey: sortKey || undefined,
- sortOrder,
- isTest: showTest,
+ ...body,
};
- const { docs, totalCount } = await searchOp({ ...searchProps, session });
+ const { docs, totalCount } = await searchPrivateCloudProducts({
+ session,
+ ...searchProps,
+ });
if (docs.length === 0) {
return NoContent();
diff --git a/app/app/api/private-cloud/products/search/route.test.ts b/app/app/api/private-cloud/products/search/route.test.ts
index c58fe1489..05a95eebb 100644
--- a/app/app/api/private-cloud/products/search/route.test.ts
+++ b/app/app/api/private-cloud/products/search/route.test.ts
@@ -1,5 +1,5 @@
import { expect } from '@jest/globals';
-import { DecisionStatus, Ministry, Cluster } from '@prisma/client';
+import { DecisionStatus, Ministry, Cluster, ProjectStatus } from '@prisma/client';
import prisma from '@/core/prisma';
import { createSamplePrivateCloudProductData } from '@/helpers/mock-resources';
import { mockNoRoleUsers, findMockUserByIdr, findOtherMockUsers } from '@/helpers/mock-users';
@@ -211,9 +211,9 @@ describe('Search Private Cloud Products - Validations', () => {
await mockSessionByRole('admin');
const res1 = await searchPrivateCloudProjects({
- ministry: Ministry.AEST,
- cluster: Cluster.CLAB,
- includeInactive: false,
+ ministries: [Ministry.AEST],
+ clusters: [Cluster.CLAB],
+ status: [ProjectStatus.ACTIVE],
});
expect(res1.status).toBe(200);
diff --git a/app/app/api/private-cloud/products/search/route.ts b/app/app/api/private-cloud/products/search/route.ts
index 428ce2a98..28a082901 100644
--- a/app/app/api/private-cloud/products/search/route.ts
+++ b/app/app/api/private-cloud/products/search/route.ts
@@ -1,38 +1,17 @@
-import { ProjectStatus } from '@prisma/client';
import _isString from 'lodash-es/isString';
import createApiHandler from '@/core/api-handler';
import { OkResponse } from '@/core/responses';
+import { searchPrivateCloudProducts } from '@/queries/private-cloud-products';
import { privateCloudProductSearchBodySchema } from '@/validation-schemas/private-cloud';
-import searchOp from '../_operations/search';
export const POST = createApiHandler({
roles: ['user'],
validations: { body: privateCloudProductSearchBodySchema },
})(async ({ session, body }) => {
- const {
- search = '',
- page = 1,
- pageSize = 5,
- ministry,
- cluster,
- includeInactive = false,
- sortKey,
- sortOrder,
- showTest,
- } = body;
-
- const data = await searchOp({
+ const { docs, totalCount } = await searchPrivateCloudProducts({
session,
- search,
- page,
- pageSize,
- ministry,
- cluster,
- status: includeInactive ? undefined : ProjectStatus.ACTIVE,
- sortKey: sortKey || undefined,
- sortOrder,
- isTest: showTest,
+ ...body,
});
- return OkResponse(data);
+ return OkResponse({ docs, totalCount });
});
diff --git a/app/app/api/v1/private-cloud/products/route.ts b/app/app/api/v1/private-cloud/products/route.ts
index 0ee0471e2..e393c68c9 100644
--- a/app/app/api/v1/private-cloud/products/route.ts
+++ b/app/app/api/v1/private-cloud/products/route.ts
@@ -1,5 +1,4 @@
import { ProjectStatus, Ministry, Cluster } from '@prisma/client';
-import { Session } from 'next-auth';
import { z } from 'zod';
import createApiHandler from '@/core/api-handler';
import { OkResponse, BadRequestResponse } from '@/core/responses';
@@ -33,13 +32,13 @@ export const GET = apiHandler(async ({ queryParams, session }) => {
const { skip, take, page } = parsePaginationParams(_page ?? defaultPage, _pageSize ?? defaultPageSize, 10);
const { docs, totalCount } = await searchPrivateCloudProducts({
- session: session as Session,
+ session,
skip,
take,
- ministry,
- cluster,
- status,
- isTest: false,
+ ministries: ministry ? [ministry] : [],
+ clusters: cluster ? [cluster] : [],
+ status: status ? [status] : [],
+ temporary: [],
});
const data = docs.map((doc) => {
diff --git a/app/app/private-cloud/products/(product)/[licencePlate]/requests/FilterPanel.tsx b/app/app/private-cloud/products/(product)/[licencePlate]/requests/FilterPanel.tsx
index c194d0acc..a64da4599 100644
--- a/app/app/private-cloud/products/(product)/[licencePlate]/requests/FilterPanel.tsx
+++ b/app/app/private-cloud/products/(product)/[licencePlate]/requests/FilterPanel.tsx
@@ -1,7 +1,8 @@
-import { Prisma } from '@prisma/client';
+import { Cluster, Ministry, Prisma } from '@prisma/client';
import { useEffect, useRef, useState } from 'react';
import { useSnapshot, subscribe } from 'valtio';
import FormToggle from '@/components/generic/checkbox/FormToggle';
+import FormMultiSelect from '@/components/generic/select/FormMultiSelect';
import FormSelect from '@/components/generic/select/FormSelect';
import { clusters, productSorts, ministryOptions } from '@/constants';
import { pageState } from './state';
@@ -33,11 +34,11 @@ export default function FilterPanel() {
};
const handleClusterChange = (value: string) => {
- pageState.cluster = value;
+ pageState.cluster = value as Cluster;
};
const handleMinistryChange = (value: string) => {
- pageState.ministry = value;
+ pageState.ministry = value as Ministry;
};
const clearFilters = () => {
@@ -51,10 +52,10 @@ export default function FilterPanel() {
sortRef.current.value = '';
}
- pageState.cluster = '';
- pageState.ministry = '';
+ pageState.cluster = undefined;
+ pageState.ministry = undefined;
pageState.sortKey = '';
- pageState.sortOrder = '';
+ pageState.sortOrder = Prisma.SortOrder.asc;
pageState.includeInactive = false;
};
diff --git a/app/app/private-cloud/products/(product)/[licencePlate]/requests/page.tsx b/app/app/private-cloud/products/(product)/[licencePlate]/requests/page.tsx
index ac38957e9..a9e6c7317 100644
--- a/app/app/private-cloud/products/(product)/[licencePlate]/requests/page.tsx
+++ b/app/app/private-cloud/products/(product)/[licencePlate]/requests/page.tsx
@@ -42,8 +42,8 @@ export default privateCloudProductRequests(({ pathParams, queryParams, session }
return (
{
pageState.page = page;
diff --git a/app/app/private-cloud/products/(product)/[licencePlate]/requests/state.ts b/app/app/private-cloud/products/(product)/[licencePlate]/requests/state.ts
index a8eb96d61..25fe5cb61 100644
--- a/app/app/private-cloud/products/(product)/[licencePlate]/requests/state.ts
+++ b/app/app/private-cloud/products/(product)/[licencePlate]/requests/state.ts
@@ -1,15 +1,16 @@
-import { Prisma } from '@prisma/client';
+import { Prisma, ProjectStatus } from '@prisma/client';
import { proxy, useSnapshot } from 'valtio';
-import { PrivateCloudProductSearchCriteria } from '@/services/backend/private-cloud/products';
+import { productSorts } from '@/constants';
+import { PrivateCloudRequestSearchBody } from '@/validation-schemas/private-cloud';
-export const pageState = proxy>({
+export const pageState = proxy({
search: '',
page: 1,
pageSize: 10,
- ministry: '',
- cluster: '',
+ ministry: undefined,
+ cluster: undefined,
includeInactive: true,
- sortKey: '',
- sortOrder: Prisma.SortOrder.desc,
- showTest: false,
+ showTest: true,
+ sortKey: productSorts[0].sortKey,
+ sortOrder: productSorts[0].sortOrder,
});
diff --git a/app/app/private-cloud/products/all/FilterPanel.tsx b/app/app/private-cloud/products/all/FilterPanel.tsx
index 526f608cb..1838abffa 100644
--- a/app/app/private-cloud/products/all/FilterPanel.tsx
+++ b/app/app/private-cloud/products/all/FilterPanel.tsx
@@ -1,132 +1,58 @@
-import { Prisma } from '@prisma/client';
-import { useRef } from 'react';
+import { Ministry, Cluster, Prisma, ProjectStatus } from '@prisma/client';
import { useSnapshot } from 'valtio';
-import FormToggle from '@/components/generic/checkbox/FormToggle';
-import FormSelect from '@/components/generic/select/FormSelect';
-import { clusters, productSorts, ministryOptions } from '@/constants';
+import FormMultiSelect from '@/components/generic/select/FormMultiSelect';
+import { clusters, ministryOptions } from '@/constants';
import { pageState } from './state';
export default function FilterPanel() {
const pageSnapshot = useSnapshot(pageState);
- const clusterProviderRef = useRef(null);
- const ministryRef = useRef(null);
- const sortRef = useRef(null);
- const toggleDeletedProductsText = 'Show Deleted Products';
- const toggleTestProductsText = 'Filter Temp Products';
-
- const handleSortChange = (value: string) => {
- const selectedOption = productSorts.find((privateSortName) => privateSortName.humanFriendlyName === value);
- if (selectedOption) {
- pageState.sortKey = selectedOption.sortKey;
- pageState.sortOrder = selectedOption.sortOrder;
- } else {
- pageState.sortKey = '';
- pageState.sortOrder = Prisma.SortOrder.desc;
- }
-
- pageState.page = 1;
- };
-
- const handleDeletedProductsToggleChange = () => {
- pageState.includeInactive = !pageSnapshot.includeInactive;
- pageState.page = 1;
- };
-
- const handleTestProductsToggleChange = () => {
- pageState.showTest = !pageSnapshot.showTest;
- pageState.page = 1;
- };
-
- const handleClusterChange = (value: string) => {
- pageState.cluster = value;
- pageState.page = 1;
- };
-
- const handleMinistryChange = (value: string) => {
- pageState.ministry = value;
- pageState.page = 1;
- };
-
- const clearFilters = () => {
- if (clusterProviderRef.current) {
- clusterProviderRef.current.value = '';
- }
- if (ministryRef.current) {
- ministryRef.current.value = '';
- }
- if (sortRef.current) {
- sortRef.current.value = '';
- }
-
- pageState.cluster = '';
- pageState.ministry = '';
- pageState.sortKey = '';
- pageState.sortOrder = '';
- pageState.includeInactive = false;
- pageState.showTest = false;
- };
return (
-
-
-
-
-
-
-
-
-
-
-
+
+ {
+ pageState.ministries = value as Ministry[];
+ pageState.page = 1;
+ }}
+ classNames={{ wrapper: 'col-span-5' }}
+ />
+ ({ label: v, value: v }))]}
+ onChange={(value) => {
+ pageState.clusters = value as Cluster[];
+ pageState.page = 1;
+ }}
+ classNames={{ wrapper: 'col-span-3' }}
+ />
+ {
+ pageState.status = value as ProjectStatus[];
+ pageState.page = 1;
+ }}
+ classNames={{ wrapper: 'col-span-2' }}
+ />
+ {
+ pageState.temporary = value as ('YES' | 'NO')[];
+ pageState.page = 1;
+ }}
+ classNames={{ wrapper: 'col-span-2' }}
+ />
);
}
diff --git a/app/app/private-cloud/products/all/page.tsx b/app/app/private-cloud/products/all/page.tsx
index 647be2eb4..9ba6f9e5c 100644
--- a/app/app/private-cloud/products/all/page.tsx
+++ b/app/app/private-cloud/products/all/page.tsx
@@ -1,9 +1,11 @@
'use client';
+import { Prisma } from '@prisma/client';
import { useQuery } from '@tanstack/react-query';
import { proxy, useSnapshot } from 'valtio';
import Table from '@/components/generic/table/Table';
import TableBodyPrivateProducts from '@/components/table/TableBodyPrivateProducts';
+import { productSorts } from '@/constants';
import createClientPage from '@/core/client-page';
import { processPrivateCloudProductData } from '@/helpers/row-mapper';
import { searchPrivateCloudProducts, downloadPrivateCloudProducts } from '@/services/backend/private-cloud/products';
@@ -37,9 +39,13 @@ export default privateCloudProducts(({ pathParams, queryParams, session }) => {
title="Products in Private Cloud OpenShift Platform"
description="These are your products hosted on Private Cloud OpenShift platform"
totalCount={totalCount}
- page={snap.page}
- pageSize={snap.pageSize}
+ page={snap.page ?? 1}
+ pageSize={snap.pageSize ?? 10}
search={snap.search}
+ sortKey={
+ (productSorts.find((v) => v.sortKey === snap.sortKey && v.sortOrder === snap.sortOrder) ?? productSorts[0])
+ ?.humanFriendlyName
+ }
onPagination={(page: number, pageSize: number) => {
pageState.page = page;
pageState.pageSize = pageSize;
@@ -52,6 +58,20 @@ export default privateCloudProducts(({ pathParams, queryParams, session }) => {
const result = await downloadPrivateCloudProducts(snap);
return result;
}}
+ onSort={(sortKey) => {
+ pageState.page = 1;
+
+ const selectedOption = productSorts.find((privateSortName) => privateSortName.humanFriendlyName === sortKey);
+
+ if (selectedOption) {
+ pageState.sortKey = selectedOption.sortKey;
+ pageState.sortOrder = selectedOption.sortOrder;
+ } else {
+ pageState.sortKey = '';
+ pageState.sortOrder = Prisma.SortOrder.desc;
+ }
+ }}
+ sortOptions={productSorts.map((v) => ({ label: v.humanFriendlyName, value: v.humanFriendlyName }))}
filters={
}
isLoading={isLoading}
>
diff --git a/app/app/private-cloud/products/all/state.ts b/app/app/private-cloud/products/all/state.ts
index 71c48d309..bf1c5707b 100644
--- a/app/app/private-cloud/products/all/state.ts
+++ b/app/app/private-cloud/products/all/state.ts
@@ -1,16 +1,16 @@
-import { Prisma } from '@prisma/client';
+import { Prisma, ProjectStatus } from '@prisma/client';
import { proxy, useSnapshot } from 'valtio';
-import { PrivateCloudProductSearchCriteria } from '@/services/backend/private-cloud/products';
+import { productSorts } from '@/constants';
+import { PrivateCloudProductSearchBody } from '@/validation-schemas/private-cloud';
-export const pageState = proxy
({
+export const pageState = proxy({
search: '',
page: 1,
pageSize: 10,
- licencePlate: '',
- ministry: '',
- cluster: '',
- includeInactive: false,
- sortKey: '',
- sortOrder: Prisma.SortOrder.desc,
- showTest: false,
+ ministries: [],
+ clusters: [],
+ status: [ProjectStatus.ACTIVE],
+ temporary: [],
+ sortKey: productSorts[0].sortKey,
+ sortOrder: productSorts[0].sortOrder,
});
diff --git a/app/app/private-cloud/requests/all/FilterPanel.tsx b/app/app/private-cloud/requests/all/FilterPanel.tsx
index 3dc766e73..ebc558d83 100644
--- a/app/app/private-cloud/requests/all/FilterPanel.tsx
+++ b/app/app/private-cloud/requests/all/FilterPanel.tsx
@@ -1,4 +1,4 @@
-import { Prisma } from '@prisma/client';
+import { Cluster, Ministry, Prisma } from '@prisma/client';
import { useRef } from 'react';
import { useSnapshot } from 'valtio';
import FormToggle from '@/components/generic/checkbox/FormToggle';
@@ -34,11 +34,11 @@ export default function FilterPanel() {
};
const handleClusterChange = (value: string) => {
- pageState.cluster = value;
+ pageState.cluster = value as Cluster;
};
const handleMinistryChange = (value: string) => {
- pageState.ministry = value;
+ pageState.ministry = value as Ministry;
};
const clearFilters = () => {
@@ -52,10 +52,10 @@ export default function FilterPanel() {
sortRef.current.value = '';
}
- pageState.cluster = '';
- pageState.ministry = '';
+ pageState.cluster = undefined;
+ pageState.ministry = undefined;
pageState.sortKey = '';
- pageState.sortOrder = '';
+ pageState.sortOrder = Prisma.SortOrder.asc;
pageState.includeInactive = false;
};
diff --git a/app/app/private-cloud/requests/all/page.tsx b/app/app/private-cloud/requests/all/page.tsx
index ef9d86402..ed88fba65 100644
--- a/app/app/private-cloud/requests/all/page.tsx
+++ b/app/app/private-cloud/requests/all/page.tsx
@@ -36,8 +36,8 @@ export default privateCloudRequests(({ pathParams, queryParams, session }) => {
title="Products in Private Cloud OpenShift Platform"
description="Products with pending requests currently under admin review."
totalCount={totalCount}
- page={snap.page}
- pageSize={snap.pageSize}
+ page={snap.page ?? 1}
+ pageSize={snap.pageSize ?? 10}
search={snap.search}
onPagination={(page: number, pageSize: number) => {
pageState.page = page;
diff --git a/app/app/private-cloud/requests/all/state.ts b/app/app/private-cloud/requests/all/state.ts
index 71c48d309..25fe5cb61 100644
--- a/app/app/private-cloud/requests/all/state.ts
+++ b/app/app/private-cloud/requests/all/state.ts
@@ -1,16 +1,16 @@
-import { Prisma } from '@prisma/client';
+import { Prisma, ProjectStatus } from '@prisma/client';
import { proxy, useSnapshot } from 'valtio';
-import { PrivateCloudProductSearchCriteria } from '@/services/backend/private-cloud/products';
+import { productSorts } from '@/constants';
+import { PrivateCloudRequestSearchBody } from '@/validation-schemas/private-cloud';
-export const pageState = proxy({
+export const pageState = proxy({
search: '',
page: 1,
pageSize: 10,
- licencePlate: '',
- ministry: '',
- cluster: '',
- includeInactive: false,
- sortKey: '',
- sortOrder: Prisma.SortOrder.desc,
- showTest: false,
+ ministry: undefined,
+ cluster: undefined,
+ includeInactive: true,
+ showTest: true,
+ sortKey: productSorts[0].sortKey,
+ sortOrder: productSorts[0].sortOrder,
});
diff --git a/app/components/generic/table/SearchFilterExport.tsx b/app/components/generic/table/SearchFilterExport.tsx
index 04ffba8c2..e138bd61b 100644
--- a/app/components/generic/table/SearchFilterExport.tsx
+++ b/app/components/generic/table/SearchFilterExport.tsx
@@ -1,21 +1,33 @@
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
-import { UnstyledButton, Tooltip } from '@mantine/core';
+import { UnstyledButton, Tooltip, ComboboxData } from '@mantine/core';
import { IconFilter, IconSearch, IconCircleX } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState, useRef } from 'react';
import ExportButton from '@/components/buttons/ExportButton';
+import LightButton from '@/components/generic/button/LightButton';
+import FormSingleSelect, { FormSingleSelectProps } from '@/components/generic/select/FormSingleSelect';
import { useDebounce } from '@/utils/hooks';
-import LightButton from '../button/LightButton';
import { useTableState } from './Table';
type Props = {
initialSearch?: string;
- onSearch?: (search: string) => void;
+ onSort?: (sortKey: string) => void;
+ sortOptions?: ComboboxData;
+ sortKey?: string;
+ onSearch?: (searchKey: string) => void;
onExport?: () => Promise;
children?: React.ReactNode;
};
-export default function SearchFilterExport({ initialSearch = '', onSearch, onExport, children }: Props) {
+export default function SearchFilterExport({
+ initialSearch = '',
+ onSort,
+ sortOptions = [],
+ sortKey = '',
+ onSearch,
+ onExport,
+ children,
+}: Props) {
const pathname = usePathname();
const { replace } = useRouter();
const searchParams = useSearchParams()!;
@@ -44,11 +56,24 @@ export default function SearchFilterExport({ initialSearch = '', onSearch, onExp
return (
-
-
-
+
+
+ {onSort && (
+ {
+ if (!value) return;
+ onSort(value);
+ }}
+ />
+ )}
+
+
+ )}
+ {children && (
+
+
+ Filter
+
)}
+ {onExport &&
}
- {children && (
-
-
- Filter
-
- )}
- {onExport &&
}
-
{children}
+
{children}
);
diff --git a/app/components/generic/table/Table.tsx b/app/components/generic/table/Table.tsx
index ab23bc7fd..b172f21db 100644
--- a/app/components/generic/table/Table.tsx
+++ b/app/components/generic/table/Table.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Button, Divider, Grid, LoadingOverlay, Box } from '@mantine/core';
+import { Button, Divider, Grid, LoadingOverlay, Box, ComboboxData } from '@mantine/core';
import { createContext, useContext, useRef, useEffect } from 'react';
import { proxy, useSnapshot } from 'valtio';
import Pagination from './Pagination';
@@ -13,6 +13,7 @@ const defaultValue = {
pageSize: 0,
totalCount: 0,
search: '',
+ sortKey: '',
onPagination: (page: number, pageSize: number) => {},
isLoading: false,
};
@@ -29,6 +30,9 @@ export default function Table({
onPagination = () => {},
onSearch,
onExport,
+ onSort,
+ sortOptions = [],
+ sortKey = '',
filters,
isLoading = false,
children,
@@ -42,6 +46,9 @@ export default function Table({
onPagination?: (page: number, pageSize: number) => void;
onSearch?: (search: string) => void;
onExport?: () => Promise;
+ onSort?: (sortKey: string) => void;
+ sortOptions?: ComboboxData;
+ sortKey?: string;
filters?: React.ReactNode;
isLoading?: boolean;
children: React.ReactNode;
@@ -53,6 +60,7 @@ export default function Table({
state.pageSize = pageSize;
state.totalCount = totalCount;
state.search = search;
+ state.sortKey = sortKey;
state.onPagination = onPagination;
state.isLoading = isLoading;
}, [state, page, pageSize, totalCount, search, onPagination, isLoading]);
@@ -62,7 +70,14 @@ export default function Table({
{(onSearch || onExport || filters) && (
-
+
{filters}
)}
diff --git a/app/constants/index.ts b/app/constants/index.ts
index 5a6b93a7a..81a737eb4 100644
--- a/app/constants/index.ts
+++ b/app/constants/index.ts
@@ -1,4 +1,4 @@
-import { Cluster, Ministry, Provider } from '@prisma/client';
+import { Cluster, Ministry, Prisma, Provider } from '@prisma/client';
import { CpuQuotaEnum, MemoryQuotaEnum, StorageQuotaEnum } from '@/validation-schemas/private-cloud';
export const clusters = Object.values(Cluster).filter((cluster) => cluster !== 'GOLDDR');
@@ -171,42 +171,42 @@ export const productSorts = [
{
sortKey: 'updatedAt',
humanFriendlyName: 'Product last update (new to old)',
- sortOrder: 'desc',
+ sortOrder: Prisma.SortOrder.desc,
},
{
sortKey: 'updatedAt',
humanFriendlyName: 'Product last update (old to new)',
- sortOrder: 'asc',
+ sortOrder: Prisma.SortOrder.asc,
},
{
sortKey: 'name',
humanFriendlyName: 'Product name (A-Z)',
- sortOrder: 'asc',
+ sortOrder: Prisma.SortOrder.asc,
},
{
sortKey: 'name',
humanFriendlyName: 'Product name (Z-A)',
- sortOrder: 'desc',
+ sortOrder: Prisma.SortOrder.desc,
},
{
sortKey: 'description',
humanFriendlyName: 'Product description (A-Z)',
- sortOrder: 'asc',
+ sortOrder: Prisma.SortOrder.asc,
},
{
sortKey: 'description',
humanFriendlyName: 'Product description (Z-A)',
- sortOrder: 'desc',
+ sortOrder: Prisma.SortOrder.desc,
},
{
sortKey: 'licencePlate',
humanFriendlyName: 'Product Licence Plate (A-Z)',
- sortOrder: 'asc',
+ sortOrder: Prisma.SortOrder.asc,
},
{
sortKey: 'licencePlate',
humanFriendlyName: 'Product Licence Plate (Z-A)',
- sortOrder: 'desc',
+ sortOrder: Prisma.SortOrder.desc,
},
];
diff --git a/app/queries/private-cloud-products.ts b/app/queries/private-cloud-products.ts
index 8340447f9..e0f40c13a 100644
--- a/app/queries/private-cloud-products.ts
+++ b/app/queries/private-cloud-products.ts
@@ -1,7 +1,10 @@
import { Ministry, Cluster, ProjectStatus, Prisma } from '@prisma/client';
+import _isNumber from 'lodash-es/isNumber';
import { Session } from 'next-auth';
import prisma from '@/core/prisma';
+import { parsePaginationParams } from '@/helpers/pagination';
import { PrivateCloudProductDetail, PrivateCloudProductDetailDecorated } from '@/types/private-cloud';
+import { PrivateCloudProductSearchBody } from '@/validation-schemas/private-cloud';
import { getMatchingUserIds } from './users';
export const privateCloudProductSimpleInclude = {
@@ -32,27 +35,26 @@ export async function searchPrivateCloudProducts({
session,
skip,
take,
- ministry,
- cluster,
+ page,
+ pageSize,
+ ministries,
+ clusters,
status,
- search,
+ temporary,
+ search = '',
sortKey = defaultSortKey,
sortOrder = Prisma.SortOrder.desc,
extraFilter,
- isTest,
-}: {
+}: PrivateCloudProductSearchBody & {
session: Session;
- skip: number;
- take: number;
- status?: ProjectStatus;
- ministry?: Ministry;
- cluster?: Cluster;
- search?: string;
- sortKey?: string;
- sortOrder?: Prisma.SortOrder;
+ skip?: number;
+ take?: number;
extraFilter?: Prisma.PrivateCloudProjectWhereInput;
- isTest: boolean;
}) {
+ if (!_isNumber(skip) && !_isNumber(take) && page && pageSize) {
+ ({ skip, take } = parsePaginationParams(page, pageSize, 10));
+ }
+
const where: Prisma.PrivateCloudProjectWhereInput = extraFilter ?? {};
const orderBy = { [sortKey || defaultSortKey]: Prisma.SortOrder[sortOrder] };
@@ -77,20 +79,20 @@ export async function searchPrivateCloudProducts({
}
}
- if (ministry) {
- where.ministry = ministry as Ministry;
+ if (ministries && ministries.length > 0) {
+ where.ministry = { in: ministries };
}
- if (cluster) {
- where.cluster = cluster as Cluster;
+ if (clusters && clusters.length > 0) {
+ where.cluster = { in: clusters };
}
- if (status) {
- where.status = status;
+ if (status && status.length > 0) {
+ where.status = { in: status };
}
- if (isTest) {
- where.isTest = isTest;
+ if (temporary && temporary.length === 1) {
+ where.isTest = temporary[0] === 'YES';
}
const [docs, totalCount] = await Promise.all([
diff --git a/app/services/backend/private-cloud/products.ts b/app/services/backend/private-cloud/products.ts
index b985e13f5..0a2176e32 100644
--- a/app/services/backend/private-cloud/products.ts
+++ b/app/services/backend/private-cloud/products.ts
@@ -7,6 +7,10 @@ import {
PrivateCloudRequestDetail,
} from '@/types/private-cloud';
import { downloadFile } from '@/utils/file-download';
+import {
+ PrivateCloudProductSearchBody,
+ PrivateCloudProductSearchNoPaginationBody,
+} from '@/validation-schemas/private-cloud';
import { instance as parentInstance } from './instance';
export const instance = axios.create({
@@ -14,25 +18,7 @@ export const instance = axios.create({
baseURL: `${parentInstance.defaults.baseURL}/products`,
});
-export interface PrivateCloudProductAllCriteria {
- search: string;
- page: number;
- pageSize: number;
- licencePlate: string;
- ministry: string;
- cluster: string;
- includeInactive: boolean;
- sortKey: string;
- sortOrder: string;
- showTest: boolean;
-}
-
-export interface PrivateCloudProductSearchCriteria extends PrivateCloudProductAllCriteria {
- page: number;
- pageSize: number;
-}
-
-export async function searchPrivateCloudProducts(data: PrivateCloudProductSearchCriteria) {
+export async function searchPrivateCloudProducts(data: PrivateCloudProductSearchBody) {
const result = await instance.post('/search', data).then((res) => {
return res.data;
});
@@ -40,7 +26,7 @@ export async function searchPrivateCloudProducts(data: PrivateCloudProductSearch
return result as PrivateCloudProductSearch;
}
-export async function downloadPrivateCloudProducts(data: PrivateCloudProductAllCriteria) {
+export async function downloadPrivateCloudProducts(data: PrivateCloudProductSearchNoPaginationBody) {
const result = await instance.post('/download', data, { responseType: 'blob' }).then((res) => {
if (res.status === 204) return false;
diff --git a/app/services/backend/private-cloud/requests.ts b/app/services/backend/private-cloud/requests.ts
index 4d958fe9c..48de78a53 100644
--- a/app/services/backend/private-cloud/requests.ts
+++ b/app/services/backend/private-cloud/requests.ts
@@ -4,8 +4,8 @@ import {
PrivateCloudRequestDetailDecorated,
PrivateCloudRequestSearch,
} from '@/types/private-cloud';
+import { PrivateCloudRequestSearchBody } from '@/validation-schemas/private-cloud';
import { instance as parentInstance } from './instance';
-import { PrivateCloudProductSearchCriteria } from './products';
export const instance = axios.create({
...parentInstance.defaults,
@@ -32,7 +32,7 @@ export async function getPrivateCloudRequest(id: string) {
return result as PrivateCloudRequestDetailDecorated;
}
-export async function searchPrivateCloudRequests(data: PrivateCloudProductSearchCriteria) {
+export async function searchPrivateCloudRequests(data: PrivateCloudRequestSearchBody) {
const result = await instance.post('/search', data).then((res) => {
return res.data;
});
diff --git a/app/types/next-auth.d.ts b/app/types/next-auth.d.ts
index 6f422a583..2ca00812a 100644
--- a/app/types/next-auth.d.ts
+++ b/app/types/next-auth.d.ts
@@ -101,3 +101,9 @@ declare module 'next-auth/jwt' {
roles?: string[];
}
}
+
+// This issue requires further investigation.
+// See https://github.com/pmndrs/valtio/issues/327#issuecomment-1035937848
+declare module 'valtio' {
+ function useSnapshot(p: T): T;
+}
diff --git a/app/validation-schemas/private-cloud.ts b/app/validation-schemas/private-cloud.ts
index 747e8b604..b8926e319 100644
--- a/app/validation-schemas/private-cloud.ts
+++ b/app/validation-schemas/private-cloud.ts
@@ -1,4 +1,4 @@
-import { Cluster, Ministry, Prisma } from '@prisma/client';
+import { Cluster, Ministry, Prisma, ProjectStatus } from '@prisma/client';
import _isString from 'lodash-es/isString';
import { string, z } from 'zod';
import { phoneNumberRegex } from '@/constants/regex';
@@ -135,12 +135,12 @@ export const privateCloudRequestDecisionBodySchema = privateCloudEditRequestBody
export const privateCloudProductSearchNoPaginationBodySchema = z.object({
search: z.string().optional(),
- ministry: z.preprocess(processUpperEnumString, z.nativeEnum(Ministry).optional()),
- cluster: z.preprocess(processUpperEnumString, z.nativeEnum(Cluster).optional()),
- includeInactive: z.boolean().optional(),
+ ministries: z.array(z.nativeEnum(Ministry)).optional(),
+ clusters: z.array(z.nativeEnum(Cluster)).optional(),
+ status: z.array(z.nativeEnum(ProjectStatus)).optional(),
+ temporary: z.array(z.enum(['YES', 'NO'])).optional(),
sortKey: z.string().optional(),
sortOrder: z.preprocess(processEnumString, z.nativeEnum(Prisma.SortOrder).optional()),
- showTest: z.boolean().default(false),
});
export const privateCloudProductSearchBodySchema = privateCloudProductSearchNoPaginationBodySchema.merge(