grid.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import * as React from "react";
  2. import { DetailsList, DetailsListLayoutMode, Dropdown, IColumn, ICommandBarItemProps, IDetailsHeaderProps, IStackProps, Label, Selection, Stack, TooltipHost, TooltipOverflowMode } from "@fluentui/react";
  3. import { useConst } from "@fluentui/react-hooks";
  4. import { AlphaNumSortMemory, QueryRequest, QuerySortItem } from "src/Memory";
  5. import * as ESPRequest from "src/ESPRequest";
  6. import nlsHPCC from "src/nlsHPCC";
  7. import { createCopyDownloadSelection } from "../components/Common";
  8. import { DojoGrid } from "../components/DojoGrid";
  9. import { useDeepCallback, useDeepEffect } from "./deepHooks";
  10. import { Pagination } from "@fluentui/react-experiments/lib/Pagination";
  11. import { useUserStore } from "./store";
  12. interface useGridProps {
  13. store: any,
  14. query?: QueryRequest,
  15. sort?: QuerySortItem,
  16. columns: object,
  17. getSelected?: () => any[],
  18. filename: string
  19. }
  20. interface useGridResponse {
  21. Grid: React.FunctionComponent,
  22. selection: any[],
  23. refreshTable: (clearSelection?: boolean) => void,
  24. copyButtons: ICommandBarItemProps[]
  25. }
  26. export function useGrid({
  27. store,
  28. query,
  29. sort,
  30. columns,
  31. getSelected,
  32. filename
  33. }: useGridProps): useGridResponse {
  34. const constStore = useConst(store);
  35. const constQuery = useConst({ ...query });
  36. const constSort = useConst({ ...sort });
  37. const constColumns = useConst({ ...columns });
  38. const constGetSelected = useConst(() => getSelected);
  39. const [grid, setGrid] = React.useState<any>(undefined);
  40. const [selection, setSelection] = React.useState([]);
  41. const Grid = React.useCallback(() => <DojoGrid
  42. store={constStore}
  43. query={constQuery}
  44. sort={constSort}
  45. columns={constColumns}
  46. getSelected={constGetSelected}
  47. setGrid={setGrid}
  48. setSelection={setSelection} />,
  49. [constColumns, constGetSelected, constQuery, constSort, constStore]);
  50. const refreshTable = useDeepCallback((clearSelection = false) => {
  51. grid?.set("query", { ...query });
  52. if (clearSelection) {
  53. grid?.clearSelection();
  54. }
  55. }, [grid], [query]);
  56. useDeepEffect(() => {
  57. refreshTable();
  58. }, [], [query]);
  59. const copyButtons = React.useMemo((): ICommandBarItemProps[] => [
  60. ...createCopyDownloadSelection(constColumns, selection, `${filename}.csv`)
  61. ], [constColumns, filename, selection]);
  62. return { Grid, selection, refreshTable, copyButtons };
  63. }
  64. function tooltipItemRenderer(item: any, index: number, column: IColumn) {
  65. const id = `${column.key}-${index}`;
  66. const value = item[column.fieldName || column.key];
  67. return <TooltipHost id={id} content={value} overflowMode={TooltipOverflowMode.Parent}>
  68. {column.data.formatter ?
  69. <span style={{ display: "flex" }}>{column.data.formatter(value, item)}</span> :
  70. <span aria-describedby={id}>{value}</span>
  71. }
  72. </TooltipHost>;
  73. }
  74. function columnsAdapter(columns, sorted?: QuerySortItem): IColumn[] {
  75. const attr = sorted?.attribute;
  76. const desc = sorted?.descending;
  77. const retVal: IColumn[] = [];
  78. for (const key in columns) {
  79. const column = columns[key];
  80. if (column?.selectorType === undefined) {
  81. retVal.push({
  82. key,
  83. name: column.label ?? key,
  84. fieldName: column.field ?? key,
  85. minWidth: column.width,
  86. maxWidth: column.width,
  87. isResizable: true,
  88. isSorted: key == attr,
  89. isSortedDescending: key == attr && desc,
  90. iconName: column.headerIcon,
  91. isIconOnly: !!column.headerIcon,
  92. data: column,
  93. onRender: tooltipItemRenderer
  94. } as IColumn);
  95. }
  96. }
  97. return retVal;
  98. }
  99. interface useFluentStoreGridProps {
  100. store: any,
  101. query?: QueryRequest,
  102. sort?: QuerySortItem,
  103. start?: number,
  104. count?: number,
  105. columns: object,
  106. filename: string
  107. }
  108. interface useFluentStoreGridResponse {
  109. Grid: React.FunctionComponent<{ height?: string }>,
  110. selection: any[],
  111. copyButtons: ICommandBarItemProps[],
  112. total: number,
  113. refreshTable: () => void
  114. }
  115. function useFluentStoreGrid({
  116. store,
  117. query,
  118. sort,
  119. start,
  120. count,
  121. columns,
  122. filename
  123. }: useFluentStoreGridProps): useFluentStoreGridResponse {
  124. const constColumns = useConst({ ...columns });
  125. const [sorted, setSorted] = React.useState<QuerySortItem>(sort);
  126. const [selection, setSelection] = React.useState([]);
  127. const [items, setItems] = React.useState<any[]>([]);
  128. const [total, setTotal] = React.useState<number>();
  129. const refreshTable = React.useCallback(() => {
  130. if (isNaN(start) || isNaN(count)) return;
  131. const storeQuery = store.query(query ?? {}, { start, count, sort: sorted ? [sorted] : undefined });
  132. storeQuery.total.then(total => {
  133. setTotal(total);
  134. });
  135. storeQuery.then(items => {
  136. setItems(items);
  137. });
  138. }, [count, query, sorted, start, store]);
  139. React.useEffect(() => {
  140. refreshTable();
  141. }, [refreshTable]);
  142. const fluentColumns: IColumn[] = React.useMemo(() => {
  143. return columnsAdapter(constColumns, sorted);
  144. }, [constColumns, sorted]);
  145. const onColumnClick = React.useCallback((event: React.MouseEvent<HTMLElement>, column: IColumn) => {
  146. if (constColumns[column.key]?.sortable === false) return;
  147. let sorted = column.isSorted;
  148. let isSortedDescending: boolean = column.isSortedDescending;
  149. if (!sorted) {
  150. sorted = true;
  151. isSortedDescending = false;
  152. } else if (!isSortedDescending) {
  153. isSortedDescending = true;
  154. } else {
  155. sorted = false;
  156. isSortedDescending = false;
  157. }
  158. setSorted({
  159. attribute: sorted ? column.key : "",
  160. descending: sorted ? isSortedDescending : false
  161. });
  162. }, [constColumns]);
  163. const selectionHandler = useConst(new Selection({
  164. onSelectionChanged: () => {
  165. setSelection(selectionHandler.getSelection());
  166. }
  167. }));
  168. const renderDetailsHeader = React.useCallback((props: IDetailsHeaderProps, defaultRender?: any) => {
  169. return defaultRender({
  170. ...props,
  171. onRenderColumnHeaderTooltip: (tooltipHostProps) => {
  172. return <TooltipHost {...tooltipHostProps} content={tooltipHostProps?.column?.data?.headerTooltip ?? ""} />;
  173. },
  174. styles: { root: { paddingTop: 1 } }
  175. });
  176. }, []);
  177. const Grid = React.useCallback(({ height }) => <DetailsList
  178. compact={true}
  179. items={items}
  180. columns={fluentColumns}
  181. setKey="set"
  182. layoutMode={DetailsListLayoutMode.justified}
  183. selection={selectionHandler}
  184. selectionPreservedOnEmptyClick={true}
  185. onItemInvoked={this._onItemInvoked}
  186. onColumnHeaderClick={onColumnClick}
  187. onRenderDetailsHeader={renderDetailsHeader}
  188. styles={{ root: { height, minHeight: height, maxHeight: height } }}
  189. />, [fluentColumns, items, onColumnClick, renderDetailsHeader, selectionHandler]);
  190. const copyButtons = React.useMemo((): ICommandBarItemProps[] => [
  191. ...createCopyDownloadSelection(constColumns, selection, `${filename}.csv`)
  192. ], [constColumns, filename, selection]);
  193. return { Grid, selection, copyButtons, total, refreshTable };
  194. }
  195. interface useFluentGridProps {
  196. data: any[],
  197. primaryID: string,
  198. alphaNumColumns?: { [id: string]: boolean },
  199. sort?: QuerySortItem,
  200. columns: object,
  201. filename: string
  202. }
  203. export function useFluentGrid({
  204. data,
  205. primaryID,
  206. alphaNumColumns,
  207. sort,
  208. columns,
  209. filename
  210. }: useFluentGridProps): useFluentStoreGridResponse {
  211. const constStore = useConst(new AlphaNumSortMemory(primaryID, alphaNumColumns));
  212. const { Grid, selection, copyButtons, total, refreshTable } = useFluentStoreGrid({ store: constStore, columns, sort, filename });
  213. React.useEffect(() => {
  214. constStore.setData(data);
  215. refreshTable();
  216. }, [constStore, data, refreshTable]);
  217. return { Grid, selection, copyButtons, total, refreshTable };
  218. }
  219. interface useFluentPagedGridProps {
  220. persistID: string,
  221. store: ESPRequest.Store,
  222. query?: QueryRequest,
  223. sort?: QuerySortItem,
  224. columns: object,
  225. filename: string
  226. }
  227. interface useFluentPagedGridResponse {
  228. Grid: React.FunctionComponent<{ height?: string }>,
  229. GridPagination: React.FunctionComponent<Partial<IStackProps>>,
  230. selection: any[],
  231. refreshTable: (full?: boolean) => void,
  232. copyButtons: ICommandBarItemProps[]
  233. }
  234. export function useFluentPagedGrid({
  235. persistID,
  236. store,
  237. query,
  238. sort,
  239. columns,
  240. filename
  241. }: useFluentPagedGridProps): useFluentPagedGridResponse {
  242. const [page, setPage] = React.useState(0);
  243. const [persistedPageSize, setPersistedPageSize] = useUserStore(`${persistID}_pageSize`, "25");
  244. const pageSize = React.useMemo(() => {
  245. return parseInt(persistedPageSize);
  246. }, [persistedPageSize]);
  247. const { Grid, selection, copyButtons, total, refreshTable } = useFluentStoreGrid({ store, query, sort, start: page * pageSize, count: pageSize, columns, filename });
  248. const dropdownChange = React.useCallback((evt, option) => {
  249. const newPageSize = option.key as number;
  250. setPage(Math.floor((page * pageSize) / newPageSize));
  251. setPersistedPageSize(`${newPageSize}`);
  252. }, [page, pageSize, setPersistedPageSize]);
  253. const GridPagination = React.useCallback(() => {
  254. return <Stack horizontal horizontalAlign="space-between">
  255. <Stack.Item>
  256. </Stack.Item>
  257. <Stack.Item>
  258. <Pagination selectedPageIndex={page} itemsPerPage={pageSize} totalItemCount={total} pageCount={Math.ceil(total / pageSize)} format="buttons" onPageChange={index => setPage(Math.round(index))} />
  259. </Stack.Item>
  260. <Stack.Item align="center">
  261. <Label htmlFor={"pageSize"}>{nlsHPCC.PageSize}</Label>
  262. <Dropdown id="pageSize" options={[
  263. { key: 10, text: "10" },
  264. { key: 25, text: "25" },
  265. { key: 50, text: "50" },
  266. { key: 100, text: "100" },
  267. { key: 250, text: "250" },
  268. { key: 500, text: "500" },
  269. { key: 1000, text: "1000" }
  270. ]} selectedKey={pageSize} onChange={dropdownChange} />
  271. </Stack.Item>
  272. </Stack>;
  273. }, [dropdownChange, page, pageSize, total]);
  274. return { Grid, GridPagination, selection, refreshTable, copyButtons };
  275. }