From f7b5cab3506516a7c8ab250faca84937dd3f2e0b Mon Sep 17 00:00:00 2001 From: Paul Kim <141361476+pkim-gswell@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:00:42 -0600 Subject: [PATCH] feat(os): sorting + hash state (#98) * feat(os): sorting * feat(os): sorting --- src/packages/shared-types/opensearch.ts | 10 +- src/services/ui/package.json | 3 + src/services/ui/src/api/useSearch.ts | 6 +- .../ui/src/components/ExportButton/index.tsx | 13 +- .../components/Opensearch/Filtering/index.tsx | 44 +++++ .../components/Opensearch/Provider/index.tsx | 22 +++ .../Opensearch/Settings/Visibility.tsx | 64 ++++++ .../components/Opensearch/Settings/index.ts | 1 + .../src/components/Opensearch/Table/index.tsx | 78 ++++++++ .../src/components/Opensearch/Table/types.ts | 9 + .../ui/src/components/Opensearch/index.ts | 4 + .../components/Opensearch/useOpensearch.ts | 83 ++++++++ .../ui/src/components/Opensearch/utils.ts | 38 ++-- .../ui/src/components/Pagination/index.tsx | 5 +- .../ui/src/components/Popover/index.tsx | 35 ++++ .../ui/src/components/SearchForm/index.tsx | 25 +-- .../ui/src/components/Table/index.tsx | 26 ++- src/services/ui/src/components/Tabs/index.tsx | 57 ++++++ src/services/ui/src/hooks/index.ts | 1 + src/services/ui/src/hooks/useParams.ts | 59 ++++++ .../src/pages/dashboard/Lists/spas/consts.tsx | 63 ++++++ .../src/pages/dashboard/Lists/spas/index.tsx | 184 ++++-------------- .../pages/dashboard/Lists/waivers/consts.tsx | 63 ++++++ .../pages/dashboard/Lists/waivers/index.tsx | 182 +++-------------- src/services/ui/src/pages/dashboard/index.tsx | 76 ++++++-- .../ui/src/utils/createContextProvider.ts | 56 ++++++ src/services/ui/src/utils/index.ts | 1 + yarn.lock | 147 ++++++++++++++ 28 files changed, 995 insertions(+), 360 deletions(-) create mode 100644 src/services/ui/src/components/Opensearch/Filtering/index.tsx create mode 100644 src/services/ui/src/components/Opensearch/Provider/index.tsx create mode 100644 src/services/ui/src/components/Opensearch/Settings/Visibility.tsx create mode 100644 src/services/ui/src/components/Opensearch/Settings/index.ts create mode 100644 src/services/ui/src/components/Opensearch/Table/index.tsx create mode 100644 src/services/ui/src/components/Opensearch/Table/types.ts create mode 100644 src/services/ui/src/components/Opensearch/index.ts create mode 100644 src/services/ui/src/components/Opensearch/useOpensearch.ts create mode 100644 src/services/ui/src/components/Popover/index.tsx create mode 100644 src/services/ui/src/components/Tabs/index.tsx create mode 100644 src/services/ui/src/hooks/useParams.ts create mode 100644 src/services/ui/src/pages/dashboard/Lists/spas/consts.tsx create mode 100644 src/services/ui/src/pages/dashboard/Lists/waivers/consts.tsx create mode 100644 src/services/ui/src/utils/createContextProvider.ts diff --git a/src/packages/shared-types/opensearch.ts b/src/packages/shared-types/opensearch.ts index da85441d6..e64447216 100644 --- a/src/packages/shared-types/opensearch.ts +++ b/src/packages/shared-types/opensearch.ts @@ -51,16 +51,18 @@ export type OsField = export type OsFilterable = { type: OsFilterType; - field: OsField; + field: OsField | ""; value: OsFilterValue; prefix: "must" | "must_not" | "should" | "filter"; }; export type OsQueryState = { - sort: { field: string; order: "asc" | "desc" }; + sort: { field: OsField; order: "asc" | "desc" }; pagination: { number: number; size: number }; - buckets: Record; - data: T[]; + filters: OsFilterable[]; + search?: string; + // buckets: Record; + // data: T[]; }; export type OsAggregateQuery = Record< diff --git a/src/services/ui/package.json b/src/services/ui/package.json index 441fce77c..d74bf417e 100644 --- a/src/services/ui/package.json +++ b/src/services/ui/package.json @@ -28,6 +28,8 @@ "@mui/system": "^5.14.1", "@mui/x-data-grid": "^6.10.0", "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^4.29.1", "@tanstack/react-query-devtools": "^4.29.5", @@ -40,6 +42,7 @@ "file-saver": "^2.0.5", "framer-motion": "^10.16.1", "jszip": "^3.10.1", + "lz-string": "^1.5.0", "lucide-react": "^0.268.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/services/ui/src/api/useSearch.ts b/src/services/ui/src/api/useSearch.ts index 8cdec1e1f..b7c4673e8 100644 --- a/src/services/ui/src/api/useSearch.ts +++ b/src/services/ui/src/api/useSearch.ts @@ -6,10 +6,10 @@ import { import { useMutation, UseMutationOptions } from "@tanstack/react-query"; import { API } from "aws-amplify"; import { ReactQueryApiError, SearchData } from "shared-types"; -import type { OsFilterable, OsQueryState } from "shared-types"; +import type { OsQueryState, OsFilterable } from "shared-types"; type QueryProps = { - filters: OsFilterable[]; + filters: OsQueryState["filters"]; sort?: OsQueryState["sort"]; pagination: OsQueryState["pagination"]; }; @@ -57,7 +57,7 @@ export const getAllSearchData = async ( return allHits.flat(); }; -export const useSearch = ( +export const useOsSearch = ( options?: UseMutationOptions ) => { return useMutation( diff --git a/src/services/ui/src/components/ExportButton/index.tsx b/src/services/ui/src/components/ExportButton/index.tsx index 3d804b1fc..4d2023c19 100644 --- a/src/services/ui/src/components/ExportButton/index.tsx +++ b/src/services/ui/src/components/ExportButton/index.tsx @@ -5,18 +5,19 @@ import { Download, Loader } from "lucide-react"; import { useState } from "react"; import { motion } from "framer-motion"; import { format } from "date-fns"; +import { useOsParams } from "../Opensearch"; -type Props = { - type: "waiver" | "spa"; -}; - -export const ExportButton = ({ type }: Props) => { +export const OsExportButton = () => { const [loading, setLoading] = useState(false); + const params = useOsParams(); const handleExport = async () => { const csvExporter = new ExportToCsv({ useKeysAsHeaders: true, - filename: `${type}-export-${format(new Date(), "MM/dd/yyyy")}`, + filename: `${params.state.tab}-export-${format( + new Date(), + "MM/dd/yyyy" + )}`, }); setLoading(true); diff --git a/src/services/ui/src/components/Opensearch/Filtering/index.tsx b/src/services/ui/src/components/Opensearch/Filtering/index.tsx new file mode 100644 index 000000000..2f231f2be --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Filtering/index.tsx @@ -0,0 +1,44 @@ +import { + Sheet, + SheetContent, + SheetHeader, + SheetTrigger, +} from "@/components/Sheet"; +import { SearchForm } from "@/components"; +import { FC } from "react"; +import { Icon, Typography } from "@enterprise-cmcs/macpro-ux-lib"; +import { useOsParams } from "../useOpensearch"; +import { OsExportButton } from "@/components/ExportButton"; + +export const OsFiltering: FC<{ disabled?: boolean }> = (props) => { + const params = useOsParams(); + + return ( +
+ + params.onSet((s) => ({ + ...s, + pagination: { ...s.pagination, number: 0 }, + search, + })) + } + disabled={!!props.disabled} + /> + + + +
+ + Filters +
+
+ + + Filters + + +
+
+ ); +}; diff --git a/src/services/ui/src/components/Opensearch/Provider/index.tsx b/src/services/ui/src/components/Opensearch/Provider/index.tsx new file mode 100644 index 000000000..b89793547 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Provider/index.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from "react"; +import { createContextProvider } from "@/utils"; +import { ReactQueryApiError, SearchData } from "shared-types"; + +type ContextState = { + data: SearchData | undefined; + isLoading: boolean; + error: ReactQueryApiError | null; +}; + +export const [OsContextProvider, useOsContext] = + createContextProvider({ + name: "OsSearch Context", + errorMessage: "forgot to wrap with OsProvider", + }); + +export const OsProvider = (props: { + children: ReactNode; + value: ContextState; +}) => { + return ; +}; diff --git a/src/services/ui/src/components/Opensearch/Settings/Visibility.tsx b/src/services/ui/src/components/Opensearch/Settings/Visibility.tsx new file mode 100644 index 000000000..ab03cc256 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Settings/Visibility.tsx @@ -0,0 +1,64 @@ +import { cn } from "@/lib/utils"; +import { Icon, Typography } from "@enterprise-cmcs/macpro-ux-lib"; +import * as UI from "@/components/Popover"; + +type Item = { label: string; field: string; hidden: boolean }; + +type Props = { + list: T[]; + onItemClick: (field: string) => void; +}; + +export const VisibilityPopover = (props: Props) => { + return ( + + + + + +
+ +
+
+
+ ); +}; + +export const VisiblityItem = ( + props: T & { onClick: () => void } +) => { + return ( +
+ + + {props.label} + +
+ ); +}; + +export const VisibilityMenu = (props: Props) => { + return ( +
+ {props.list.map((IT) => ( + props.onItemClick(IT.field)} + {...IT} + /> + ))} +
+ ); +}; diff --git a/src/services/ui/src/components/Opensearch/Settings/index.ts b/src/services/ui/src/components/Opensearch/Settings/index.ts new file mode 100644 index 000000000..b4d75953d --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Settings/index.ts @@ -0,0 +1 @@ +export * from "./Visibility"; diff --git a/src/services/ui/src/components/Opensearch/Table/index.tsx b/src/services/ui/src/components/Opensearch/Table/index.tsx new file mode 100644 index 000000000..191961663 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Table/index.tsx @@ -0,0 +1,78 @@ +import * as UI from "@/components/Table"; +import { FC, useState } from "react"; +import { OsTableColumn } from "./types"; +import { useOsContext } from "../Provider"; +import { useOsParams } from "../useOpensearch"; +import { VisibilityPopover } from "../Settings"; + +export const OsTable: FC<{ + columns: OsTableColumn[]; +}> = (props) => { + const context = useOsContext(); + const params = useOsParams(); + const [osColumns, setOsColumns] = useState( + props.columns.map((COL) => ({ ...COL, hidden: false })) + ); + + const onToggle = (field: string) => { + setOsColumns((state) => { + return state?.map((S) => { + if (S.field !== field) return S; + return { ...S, hidden: !S.hidden }; + }); + }); + }; + + return ( + + + + {osColumns.map((TH) => { + if (TH.hidden) return null; + return ( + + params.onSet((s) => ({ + ...s, + sort: { + field: TH.field, + order: s.sort.order === "desc" ? "asc" : "desc", + }, + })) + } + > + {TH.label} + + ); + })} + + } + /> + + + + {context.data?.hits.map((DAT) => ( + + {osColumns.map((COL) => { + if (COL.hidden) return null; + return ( + + {COL.cell(DAT._source)} + + ); + })} + + ))} + + + ); +}; diff --git a/src/services/ui/src/components/Opensearch/Table/types.ts b/src/services/ui/src/components/Opensearch/Table/types.ts new file mode 100644 index 000000000..f5f463020 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/Table/types.ts @@ -0,0 +1,9 @@ +import type { OsField, OsHit, OsMainSourceItem } from "shared-types"; +import type { ReactNode } from "react"; + +export type OsTableColumn = { + field: OsField; + label: string; + props?: any; + cell: (data: OsHit["_source"]) => ReactNode; +}; diff --git a/src/services/ui/src/components/Opensearch/index.ts b/src/services/ui/src/components/Opensearch/index.ts new file mode 100644 index 000000000..ca2f6ac20 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/index.ts @@ -0,0 +1,4 @@ +export * from "./useOpensearch"; +export * from "./Table"; +export * from "./Filtering"; +export * from "./Provider"; diff --git a/src/services/ui/src/components/Opensearch/useOpensearch.ts b/src/services/ui/src/components/Opensearch/useOpensearch.ts new file mode 100644 index 000000000..504969b15 --- /dev/null +++ b/src/services/ui/src/components/Opensearch/useOpensearch.ts @@ -0,0 +1,83 @@ +import { useOsSearch } from "@/api"; +import { useParams } from "@/hooks/useParams"; +import { useEffect } from "react"; +import { OsQueryState } from "shared-types"; +import { createSearchFilterable } from "./utils"; + +type OsTab = "waivers" | "spas"; + +const DEFAULT_FILTERS: Record> = { + spas: { + filters: [ + { + field: "authority.keyword", + type: "terms", + value: ["CHIP", "MEDICAID"], + prefix: "must", + }, + ], + }, + waivers: { + filters: [ + { + field: "authority.keyword", + type: "terms", + value: ["WAIVER"], + prefix: "must", + }, + ], + }, +}; + +/** + * + * @summary + * use with main + * Comments + * - TODO: add index scope + * - FIX: Initial render fires useEffect twice - 2 os requests + */ +export const useOsQuery = (init?: Partial) => { + const params = useOsParams(init); + const { data, mutateAsync, isLoading, error } = useOsSearch(); + + const onRequest = async (query: OsQueryState, options?: any) => { + try { + await mutateAsync( + { + pagination: query.pagination, + ...(!query.search && { sort: query.sort }), + filters: [ + ...query.filters, + ...createSearchFilterable(query.search || ""), + ...(DEFAULT_FILTERS[params.state.tab].filters || []), + ], + }, + options + ); + } catch (error) { + console.error("Error occurred during search:", error); + } + }; + + useEffect(() => { + onRequest(params.state); + }, [params.queryString]); + + return { data, isLoading, error, ...params }; +}; + +export type OsParamsState = OsQueryState & { tab: OsTab }; + +export const useOsParams = (init?: Partial) => { + return useParams({ + key: "os", + initValue: { + filters: [], + search: "", + tab: "spas", + pagination: { number: 0, size: 100 }, + sort: { field: "changedDate", order: "desc" }, + }, + }); +}; diff --git a/src/services/ui/src/components/Opensearch/utils.ts b/src/services/ui/src/components/Opensearch/utils.ts index f98e41d71..dda9d1746 100644 --- a/src/services/ui/src/components/Opensearch/utils.ts +++ b/src/services/ui/src/components/Opensearch/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ /* eslint-disable camelcase */ -import { OsAggregations, OsFilterable, OsQueryState } from "shared-types"; +import { OsFilterable, OsQueryState } from "shared-types"; const filterMapQueryReducer = ( state: Record, @@ -28,7 +28,11 @@ const filterMapQueryReducer = ( if (filter.value) { state[filter.prefix].push({ query_string: { - fields: ["id", "submitterName", "leadAnalyst"], + fields: [ + "id.keyword", + "submitterName.keyword", + "leadAnalyst.keyword", + ], query: `(${filter.value}) OR (*${filter.value}*)`, }, }); @@ -71,15 +75,27 @@ export const sortQueryBuilder = (sort: OsQueryState["sort"]) => { return { sort: [{ [sort.field]: sort.order }] }; }; -export const createBucketOptions = (aggregations: OsAggregations) => { - return Object.entries(aggregations).reduce((ACC, [key, value]) => { - if (!Array.isArray(value?.buckets)) return ACC; +// export const createBucketOptions = (aggregations: OsAggregations) => { +// return Object.entries(aggregations).reduce((ACC, [key, value]) => { +// if (!Array.isArray(value?.buckets)) return ACC; - ACC[key] = value.buckets.map((BUCK) => ({ - label: `${BUCK.key} (${BUCK.doc_count})`, - value: BUCK.key, - })); +// ACC[key] = value.buckets.map((BUCK) => ({ +// label: `${BUCK.key} (${BUCK.doc_count})`, +// value: BUCK.key, +// })); - return ACC; - }, {} as OsQueryState["buckets"]); +// return ACC; +// }, {} as OsQueryState["buckets"]); +// }; + +export const createSearchFilterable = (value: string) => { + if (!value) return []; + return [ + { + type: "global_search", + field: "", + value, + prefix: "must", + } as const, + ]; }; diff --git a/src/services/ui/src/components/Pagination/index.tsx b/src/services/ui/src/components/Pagination/index.tsx index 374f26360..00b61e9ec 100644 --- a/src/services/ui/src/components/Pagination/index.tsx +++ b/src/services/ui/src/components/Pagination/index.tsx @@ -71,7 +71,10 @@ export const Pagination: FC = (props) => { {state.pageRange.map((PAGE) => { if (Array.isArray(PAGE)) return ( -