@oneuptime/common 12.0.9 → 12.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Models/DatabaseModels/RunnerJob.ts +17 -2
  4. package/Server/API/LlmProviderAPI.ts +8 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
  8. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  9. package/Server/Services/NetworkDeviceService.ts +265 -12
  10. package/Server/Services/RunnerJobService.ts +24 -1
  11. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  12. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  13. package/Server/Utils/LLM/LLMService.ts +108 -19
  14. package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
  15. package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
  16. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
  17. package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
  18. package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
  19. package/Server/Utils/SSRFProtection.ts +5 -4
  20. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  21. package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
  22. package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
  23. package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
  24. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  25. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
  26. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  27. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  28. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  29. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  30. package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
  31. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
  32. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  33. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  34. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  35. package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
  36. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  37. package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
  38. package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
  39. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  40. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  41. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
  42. package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
  43. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
  44. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
  45. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
  46. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
  47. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
  48. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  49. package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
  50. package/Tests/Types/API/URLTelScheme.test.ts +261 -0
  51. package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
  52. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  53. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  54. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  55. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
  56. package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
  57. package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
  58. package/Tests/UI/Components/Button.test.tsx +279 -0
  59. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  60. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  61. package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
  62. package/Tests/UI/Components/Icon.test.tsx +57 -0
  63. package/Tests/UI/Components/List.test.tsx +20 -3
  64. package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
  65. package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
  66. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  67. package/Tests/UI/Components/Pagination.test.tsx +721 -146
  68. package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
  69. package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
  70. package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
  71. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
  72. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
  73. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
  74. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
  75. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
  76. package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
  77. package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  79. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  80. package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
  81. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  82. package/Types/API/Protocol.ts +9 -0
  83. package/Types/API/URL.ts +286 -20
  84. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  85. package/Types/CustomField/CustomFieldType.ts +28 -0
  86. package/Types/Monitor/MonitorType.ts +11 -0
  87. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  88. package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
  89. package/Types/Runbook/RunbookStepType.ts +23 -0
  90. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
  91. package/UI/Components/Button/Button.tsx +14 -22
  92. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  93. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  94. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  95. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  96. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  97. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  98. package/UI/Components/Dictionary/Dictionary.tsx +4 -39
  99. package/UI/Components/Icon/Icon.tsx +1 -1
  100. package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
  101. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
  102. package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
  103. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
  104. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
  105. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  106. package/UI/Components/Pagination/Pagination.tsx +373 -328
  107. package/UI/Components/Pagination/PaginationUtil.ts +263 -0
  108. package/UI/Components/Pagination/Screenshots/README.md +15 -0
  109. package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
  110. package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
  111. package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
  112. package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
  113. package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
  114. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
  115. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
  116. package/UI/Components/TextArea/TextArea.tsx +3 -0
  117. package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
  118. package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
  119. package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
  120. package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
  121. package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
  122. package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
  123. package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
  124. package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
  125. package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
  126. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
  127. package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
  128. package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
  129. package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
  130. package/Utils/API.ts +243 -36
  131. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  132. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  133. package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
  134. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  135. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  137. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
  139. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  140. package/build/dist/Server/API/LlmProviderAPI.js +9 -1
  141. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  146. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
  147. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  148. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  149. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  150. package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
  151. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  152. package/build/dist/Server/Services/RunnerJobService.js +20 -2
  153. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  154. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  155. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  156. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  157. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  158. package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
  159. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  160. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
  161. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  162. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
  163. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  164. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
  165. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  166. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
  167. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  168. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
  169. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
  170. package/build/dist/Server/Utils/SSRFProtection.js +5 -4
  171. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  172. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  173. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  174. package/build/dist/Types/API/Protocol.js +9 -0
  175. package/build/dist/Types/API/Protocol.js.map +1 -1
  176. package/build/dist/Types/API/URL.js +241 -18
  177. package/build/dist/Types/API/URL.js.map +1 -1
  178. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  179. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  180. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  181. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  182. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  183. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  184. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
  185. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
  186. package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
  187. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  188. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  189. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  190. package/build/dist/UI/Components/Button/Button.js +10 -16
  191. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  193. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  195. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  196. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  197. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  198. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  199. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  200. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  201. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  202. package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
  203. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  204. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  205. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  206. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
  207. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  208. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
  209. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  210. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
  211. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  212. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  213. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  214. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
  215. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  216. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  217. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  218. package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
  219. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  220. package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
  221. package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
  222. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
  226. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  227. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
  229. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
  231. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
  232. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
  233. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
  234. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
  235. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
  236. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
  237. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
  238. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
  239. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
  240. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
  241. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
  242. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
  243. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
  244. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
  245. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
  246. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
  247. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
  248. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
  249. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
  250. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
  251. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  252. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
  253. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
  254. package/build/dist/Utils/API.js +175 -34
  255. package/build/dist/Utils/API.js.map +1 -1
  256. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  257. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  258. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  259. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  260. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
  261. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
  262. package/package.json +1 -1
@@ -1,19 +1,20 @@
1
- import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
2
- import BasicFormModal from "../FormModal/BasicFormModal";
3
- import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
1
+ import Icon from "../Icon/Icon";
4
2
  import IconProp from "../../../Types/Icon/IconProp";
3
+ import Modal from "../Modal/Modal";
4
+ import PaginationUtil, {
5
+ DefaultItemsOnPageOptions,
6
+ ItemRange,
7
+ PageWindowItem,
8
+ } from "./PaginationUtil";
5
9
  import React, {
6
10
  FunctionComponent,
11
+ KeyboardEvent,
7
12
  ReactElement,
8
13
  useEffect,
14
+ useId,
9
15
  useState,
10
16
  } from "react";
11
17
 
12
- export interface PaginationNavigationItem {
13
- pageNumber: number;
14
- itemsOnPage: number;
15
- }
16
-
17
18
  export interface ComponentProps {
18
19
  currentPageNumber: number;
19
20
  totalItemsCount: number;
@@ -28,7 +29,7 @@ export interface ComponentProps {
28
29
  * Optional. Set by analytics list endpoints that skip COUNT(*) for
29
30
  * performance — `totalItemsCount` is then only a lower bound, so
30
31
  * the page-count math and "X of Y" label don't apply. When set,
31
- * we render prev/next-only with no jump-to-page modal.
32
+ * we render prev/next-only with no numbered pages.
32
33
  */
33
34
  hasMore?: boolean | undefined;
34
35
  /*
@@ -38,386 +39,430 @@ export interface ComponentProps {
38
39
  * so the printed range is clamped to rows the page can really show.
39
40
  */
40
41
  itemsOnCurrentPage?: number | undefined;
42
+ // Optional. Page sizes offered in the "rows per page" dropdown.
43
+ itemsOnPageOptions?: Array<number> | undefined;
44
+ /*
45
+ * Optional. Denser type and padding, for footers that sit under a
46
+ * compact data view (logs, traces) rather than under a full table.
47
+ */
48
+ isCompact?: boolean | undefined;
49
+ // Optional. Appended to the container, for the caller's background/border.
50
+ className?: string | undefined;
51
+ /*
52
+ * Optional. Freezes every control without claiming the view is loading or
53
+ * broken — for callers that gate paging on something of their own.
54
+ */
55
+ isDisabled?: boolean | undefined;
41
56
  }
42
57
 
43
58
  const Pagination: FunctionComponent<ComponentProps> = (
44
59
  props: ComponentProps,
45
60
  ): ReactElement => {
61
+ /*
62
+ * Has-more mode: the count is a lower bound, so there is no last page to
63
+ * link to and no "of N" to print. Prev/next and the page size are all the
64
+ * control can honestly offer.
65
+ */
46
66
  const isHasMoreMode: boolean = props.hasMore !== undefined;
47
67
 
48
- const [minPageNumber, setMinPageNumber] = useState<number>(1);
49
- const [maxPageNumber, setMaxPageNumber] = useState<number>(1);
68
+ const uniqueId: string = useId();
69
+ const itemsOnPageSelectId: string = `pagination-items-on-page-${uniqueId}`;
70
+ const goToPageInputId: string = `pagination-go-to-page-${uniqueId}`;
71
+
72
+ const totalPageCount: number = PaginationUtil.getTotalPageCount(
73
+ props.totalItemsCount,
74
+ props.itemsOnPage,
75
+ );
76
+
77
+ const currentPageNumber: number = Math.max(
78
+ Math.floor(props.currentPageNumber) || 1,
79
+ 1,
80
+ );
50
81
 
51
- const setMinAndMaxPageNumber: () => void = (): void => {
52
- setMinPageNumber(1);
53
- let maxPageNo: number =
54
- props.totalItemsCount % props.itemsOnPage === 0
55
- ? props.totalItemsCount / props.itemsOnPage + 1
56
- : props.totalItemsCount / props.itemsOnPage;
82
+ const isDisabled: boolean =
83
+ props.isLoading || props.isError || Boolean(props.isDisabled);
57
84
 
58
- if (maxPageNo < 1) {
59
- maxPageNo = 1;
85
+ const isPreviousDisabled: boolean = currentPageNumber <= 1 || isDisabled;
86
+ /*
87
+ * An empty list is one page long, so the last-page check already covers
88
+ * it - there is nowhere forward to go from page 1 of 1.
89
+ */
90
+ const isNextDisabled: boolean = isHasMoreMode
91
+ ? !props.hasMore || isDisabled
92
+ : currentPageNumber >= totalPageCount || isDisabled;
93
+
94
+ const pageWindow: Array<PageWindowItem> = isHasMoreMode
95
+ ? []
96
+ : PaginationUtil.getPageWindow(currentPageNumber, totalPageCount);
97
+
98
+ /*
99
+ * The collapsed gaps are the jump affordance: a gap is drawn exactly where
100
+ * pages the reader might want are not on the bar, so that is where asking
101
+ * for one belongs. Nothing is spent on the toolbar until they ask.
102
+ */
103
+ const [isGoToPageModalVisible, setIsGoToPageModalVisible] =
104
+ useState<boolean>(false);
105
+ const [goToPageValue, setGoToPageValue] = useState<string>("");
106
+
107
+ // A page change from anywhere (prev/next, a number, the URL) closes the box.
108
+ useEffect(() => {
109
+ setIsGoToPageModalVisible(false);
110
+ setGoToPageValue("");
111
+ }, [currentPageNumber]);
112
+
113
+ const itemRange: ItemRange = PaginationUtil.getItemRange({
114
+ currentPageNumber: currentPageNumber,
115
+ itemsOnPage: props.itemsOnPage,
116
+ totalItemsCount: props.totalItemsCount,
117
+ itemsOnCurrentPage: props.itemsOnCurrentPage,
118
+ });
119
+
120
+ const itemsOnPageOptions: Array<number> =
121
+ PaginationUtil.getItemsOnPageOptions(
122
+ props.itemsOnPage,
123
+ props.itemsOnPageOptions || DefaultItemsOnPageOptions,
124
+ );
125
+
126
+ const textSizeClassName: string = props.isCompact ? "text-xs" : "text-sm";
127
+
128
+ type NavigateToPageFunction = (pageNumber: number) => void;
129
+
130
+ const navigateToPage: NavigateToPageFunction = (pageNumber: number): void => {
131
+ if (isDisabled) {
132
+ return;
60
133
  }
61
134
 
62
- setMaxPageNumber(Math.ceil(maxPageNo));
135
+ props.onNavigateToPage(pageNumber, props.itemsOnPage);
63
136
  };
64
137
 
65
- useEffect(() => {
66
- setMinAndMaxPageNumber();
67
- }, []);
138
+ type SubmitGoToPageFunction = () => void;
68
139
 
69
- useEffect(() => {
70
- setMinAndMaxPageNumber();
71
- }, [props.totalItemsCount, props.itemsOnPage]);
140
+ const submitGoToPage: SubmitGoToPageFunction = (): void => {
141
+ const requestedPageNumber: number = Number(goToPageValue);
72
142
 
73
- const isPreviousDisabled: boolean =
74
- props.currentPageNumber === 1 || props.isLoading || props.isError;
75
- const isNextDisabled: boolean = isHasMoreMode
76
- ? !props.hasMore || props.isLoading || props.isError
77
- : props.currentPageNumber * props.itemsOnPage >= props.totalItemsCount ||
78
- props.isLoading ||
79
- props.isError;
80
- const isCurrentPageButtonDisabled: boolean = isHasMoreMode
81
- ? props.isLoading || props.isError
82
- : props.totalItemsCount === 0 || props.isLoading || props.isError;
83
-
84
- const [showPaginationModel, setShowPaginationModel] =
85
- useState<boolean>(false);
143
+ if (!requestedPageNumber) {
144
+ return;
145
+ }
146
+
147
+ setIsGoToPageModalVisible(false);
148
+ navigateToPage(
149
+ PaginationUtil.clampPageNumber(requestedPageNumber, totalPageCount),
150
+ );
151
+ };
152
+
153
+ type GetSummaryFunction = () => string;
154
+
155
+ const getSummary: GetSummaryFunction = (): string => {
156
+ const pluralLabel: string = props.pluralLabel.toLowerCase();
157
+ const singularLabel: string = props.singularLabel.toLowerCase();
158
+
159
+ if (itemRange.isEmpty) {
160
+ return `No ${pluralLabel}`;
161
+ }
162
+
163
+ const rangeText: string =
164
+ itemRange.firstItemNumber === itemRange.lastItemNumber
165
+ ? itemRange.firstItemNumber.toLocaleString()
166
+ : `${itemRange.firstItemNumber.toLocaleString()}-${itemRange.lastItemNumber.toLocaleString()}`;
167
+
168
+ if (isHasMoreMode) {
169
+ /*
170
+ * The count cannot be printed here — it is a lower bound that also
171
+ * includes the probe row the payload dropped. The trailing "+" is all
172
+ * that can be said about what comes after this page.
173
+ */
174
+ return `Showing ${rangeText}${props.hasMore ? "+" : ""} ${pluralLabel}`;
175
+ }
176
+
177
+ return `Showing ${rangeText} of ${props.totalItemsCount.toLocaleString()} ${
178
+ props.totalItemsCount === 1 ? singularLabel : pluralLabel
179
+ }`;
180
+ };
86
181
 
87
182
  /*
88
- * The range has-more mode prints. `totalItemsCount` is only a lower bound
89
- * there, so the range is clamped to what this page can prove.
183
+ * A minimum width rather than padding alone, so a row of single-digit
184
+ * pages does not read as a row of narrower buttons than the two- and
185
+ * three-digit ones beside it.
90
186
  */
91
- const alreadySeenCount: number =
92
- props.itemsOnPage * (props.currentPageNumber - 1);
93
- const firstOnPage: number = alreadySeenCount + 1;
94
- const provenOnPage: number = Math.max(
95
- props.totalItemsCount - alreadySeenCount,
96
- 0,
97
- );
98
- const lastOnPage: number =
99
- alreadySeenCount +
100
- (props.itemsOnCurrentPage === undefined
101
- ? provenOnPage
102
- : Math.min(provenOnPage, props.itemsOnCurrentPage));
187
+ const pageButtonBaseClassName: string = `relative inline-flex items-center justify-center border border-gray-300 font-medium transition-colors focus:z-20 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
188
+ props.isCompact
189
+ ? "min-w-8 px-2 py-1.5 text-xs"
190
+ : "min-w-9 px-2 py-2 text-sm"
191
+ }`;
192
+
193
+ type GetArrowButtonClassNameFunction = (isButtonDisabled: boolean) => string;
194
+
195
+ const getArrowButtonClassName: GetArrowButtonClassNameFunction = (
196
+ isButtonDisabled: boolean,
197
+ ): string => {
198
+ return `${pageButtonBaseClassName} ${
199
+ isButtonDisabled
200
+ ? "cursor-not-allowed bg-gray-50 text-gray-300"
201
+ : "cursor-pointer bg-white text-gray-500 hover:bg-gray-50 hover:text-gray-700"
202
+ }`;
203
+ };
204
+
205
+ type GetPageNumberButtonFunction = (pageNumber: number) => ReactElement;
206
+
207
+ const getPageNumberButton: GetPageNumberButtonFunction = (
208
+ pageNumber: number,
209
+ ): ReactElement => {
210
+ const isCurrentPage: boolean = pageNumber === currentPageNumber;
211
+
212
+ return (
213
+ <li className="hidden sm:flex" key={`page-${pageNumber}`}>
214
+ <button
215
+ type="button"
216
+ data-testid={`pagination-page-${pageNumber}`}
217
+ aria-label={
218
+ isCurrentPage ? `Page ${pageNumber}` : `Go to page ${pageNumber}`
219
+ }
220
+ aria-current={isCurrentPage ? "page" : undefined}
221
+ disabled={isDisabled}
222
+ onClick={() => {
223
+ if (!isCurrentPage) {
224
+ navigateToPage(pageNumber);
225
+ }
226
+ }}
227
+ className={`${pageButtonBaseClassName} ${
228
+ isCurrentPage
229
+ ? "z-10 border-indigo-500 bg-indigo-50 text-indigo-600"
230
+ : "bg-white text-gray-600 hover:bg-gray-50"
231
+ } ${
232
+ isDisabled && !isCurrentPage
233
+ ? "cursor-not-allowed text-gray-300"
234
+ : "cursor-pointer"
235
+ }`}
236
+ >
237
+ {pageNumber.toLocaleString()}
238
+ </button>
239
+ </li>
240
+ );
241
+ };
242
+
243
+ type GetEllipsisFunction = (key: string) => ReactElement;
244
+
245
+ const getEllipsis: GetEllipsisFunction = (key: string): ReactElement => {
246
+ return (
247
+ <li className="hidden sm:flex" key={key}>
248
+ <button
249
+ type="button"
250
+ data-testid={`pagination-${key}`}
251
+ aria-label="Go to a page in between"
252
+ title="Go to page"
253
+ disabled={isDisabled}
254
+ onClick={() => {
255
+ if (!isDisabled) {
256
+ setIsGoToPageModalVisible(true);
257
+ }
258
+ }}
259
+ className={`${pageButtonBaseClassName} bg-white text-gray-400 ${
260
+ isDisabled
261
+ ? "cursor-not-allowed"
262
+ : "cursor-pointer hover:bg-gray-50 hover:text-gray-600"
263
+ }`}
264
+ >
265
+ &hellip;
266
+ </button>
267
+ </li>
268
+ );
269
+ };
103
270
 
104
271
  return (
105
- <nav
106
- className="flex items-center justify-between border-t border-gray-200 bg-white px-4"
107
- data-testid={props.dataTestId}
108
- aria-label={`Pagination for ${props.pluralLabel}`}
109
- >
110
- {/* Desktop layout: Description on left, all controls on right */}
111
- <div className="hidden md:block">
112
- <p className="text-sm text-gray-500">
113
- {!props.isLoading && isHasMoreMode && (
114
- <span>
115
- {/*
116
- * An empty page has no range to print - "Showing 1 to 0" is
117
- * how that used to read.
118
- */}
119
- {lastOnPage < firstOnPage
120
- ? `No ${props.pluralLabel.toLowerCase()}.`
121
- : `Showing ${firstOnPage} to ${lastOnPage}${
122
- props.hasMore ? "+" : ""
123
- } ${props.pluralLabel.toLowerCase()}.`}
124
- </span>
125
- )}
126
- {!props.isLoading && !isHasMoreMode && (
127
- <span>
128
- {props.totalItemsCount.toLocaleString()}{" "}
129
- {props.totalItemsCount > 1
130
- ? props.pluralLabel
131
- : props.singularLabel}{" "}
132
- {`in total. Showing ${
133
- props.itemsOnPage * (props.currentPageNumber - 1) + 1
134
- } to ${
135
- props.itemsOnPage * props.currentPageNumber
136
- } on this page.`}
137
- </span>
138
- )}
272
+ <>
273
+ <nav
274
+ className={`flex flex-col gap-3 border-t border-gray-200 bg-white px-4 py-3 text-left sm:flex-row sm:items-center sm:justify-between ${
275
+ props.className || ""
276
+ }`}
277
+ data-testid={props.dataTestId}
278
+ aria-label={`Pagination for ${props.pluralLabel}`}
279
+ >
280
+ <p
281
+ className={`${textSizeClassName} shrink-0 whitespace-nowrap text-gray-500`}
282
+ data-testid="pagination-summary"
283
+ aria-live="polite"
284
+ >
285
+ {props.isLoading ? "Loading…" : getSummary()}
139
286
  </p>
140
- </div>
141
-
142
- {/* Desktop layout: All controls together on right */}
143
- <div className="hidden md:flex">
144
- <div className="inline-flex -space-x-px rounded-md shadow-sm">
145
- <div className="my-2">
146
- <Button
147
- dataTestId="show-pagination-modal-button"
148
- className="mx-2 my-2"
149
- buttonSize={ButtonSize.ExtraSmall}
150
- icon={IconProp.AdjustmentHorizontal}
151
- buttonStyle={ButtonStyleType.ICON_LIGHT}
152
- ariaLabel="Open pagination settings"
153
- onClick={() => {
154
- setShowPaginationModel(true);
155
- }}
156
- />
157
- </div>
158
287
 
159
- <ul className="flex py-3" role="list">
160
- <li className="flex">
161
- <button
162
- type="button"
163
- disabled={isPreviousDisabled}
164
- aria-label="Go to previous page"
165
- onClick={() => {
166
- let currentPageNumber: number = props.currentPageNumber;
288
+ <div className="flex flex-wrap items-center justify-start gap-x-3 gap-y-3 sm:justify-end">
289
+ <div className="flex items-center gap-2">
290
+ <label
291
+ htmlFor={itemsOnPageSelectId}
292
+ className={`${textSizeClassName} whitespace-nowrap text-gray-500`}
293
+ >
294
+ Rows per page
295
+ </label>
296
+ <div className="relative">
297
+ <select
298
+ id={itemsOnPageSelectId}
299
+ data-testid="pagination-items-on-page-select"
300
+ value={props.itemsOnPage}
301
+ disabled={isDisabled}
302
+ onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
303
+ const newItemsOnPage: number = Number(event.target.value);
167
304
 
168
- if (typeof currentPageNumber === "string") {
169
- currentPageNumber = parseInt(currentPageNumber);
305
+ if (!newItemsOnPage || newItemsOnPage === props.itemsOnPage) {
306
+ return;
170
307
  }
171
308
 
172
- if (props.onNavigateToPage && !isPreviousDisabled) {
173
- props.onNavigateToPage(
174
- currentPageNumber - 1,
175
- props.itemsOnPage,
176
- );
177
- }
309
+ /*
310
+ * Row 400 of the old page size is not row 400 of the new one,
311
+ * so resizing the page returns to the top of the list rather
312
+ * than to an offset that may no longer exist.
313
+ */
314
+ props.onNavigateToPage(1, newItemsOnPage);
178
315
  }}
179
- className={` inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
180
- isPreviousDisabled
181
- ? "bg-gray-100 cursor-not-allowed"
182
- : "hover:bg-gray-50 cursor-pointer"
183
- }`}
316
+ className={`cursor-pointer appearance-none rounded-md border border-gray-300 bg-white py-1.5 pl-2.5 pr-8 font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-400 ${textSizeClassName}`}
184
317
  >
185
- <span className="page-link">Previous</span>
186
- </button>
187
- </li>
318
+ {itemsOnPageOptions.map((option: number) => {
319
+ return (
320
+ <option key={option} value={option}>
321
+ {option}
322
+ </option>
323
+ );
324
+ })}
325
+ </select>
326
+ <span className="pointer-events-none absolute inset-y-0 right-2 flex items-center">
327
+ <Icon
328
+ icon={IconProp.ChevronDown}
329
+ className="h-4 w-4 text-gray-400"
330
+ />
331
+ </span>
332
+ </div>
333
+ </div>
334
+
335
+ <ul
336
+ className="isolate inline-flex -space-x-px rounded-md shadow-sm"
337
+ role="list"
338
+ >
188
339
  <li className="flex">
189
340
  <button
190
341
  type="button"
191
- disabled={isCurrentPageButtonDisabled}
192
- aria-current="page"
193
- aria-label={`Page ${props.currentPageNumber}, current page. Select to jump to another page.`}
194
- data-testid="current-page-link"
195
- className={` z-10 inline-flex items-center border border-x-0 border-gray-300 hover:bg-gray-50 px-4 py-2 text-sm font-medium text-text-600 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
196
- isCurrentPageButtonDisabled ? "bg-gray-100" : ""
197
- }`}
342
+ data-testid="pagination-previous-button"
343
+ disabled={isPreviousDisabled}
344
+ aria-label="Go to previous page"
198
345
  onClick={() => {
199
- if (!isHasMoreMode) {
200
- setShowPaginationModel(true);
346
+ if (!isPreviousDisabled) {
347
+ navigateToPage(currentPageNumber - 1);
201
348
  }
202
349
  }}
350
+ className={`${getArrowButtonClassName(
351
+ isPreviousDisabled,
352
+ )} rounded-l-md`}
203
353
  >
204
- <span>{props.currentPageNumber}</span>
354
+ <Icon icon={IconProp.ChevronLeft} className="h-4 w-4" />
205
355
  </button>
206
356
  </li>
207
- <li className="flex">
208
- <button
209
- type="button"
210
- disabled={isNextDisabled}
211
- aria-label="Go to next page"
212
- onClick={() => {
213
- let currentPageNumber: number = props.currentPageNumber;
214
-
215
- if (typeof currentPageNumber === "string") {
216
- currentPageNumber = parseInt(currentPageNumber);
217
- }
218
357
 
219
- if (props.onNavigateToPage && !isNextDisabled) {
220
- props.onNavigateToPage(
221
- currentPageNumber + 1,
222
- props.itemsOnPage,
223
- );
224
- }
225
- }}
226
- className={` inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
227
- isNextDisabled
228
- ? "bg-gray-100 cursor-not-allowed"
229
- : " hover:bg-gray-50 cursor-pointer"
230
- }`}
358
+ {/*
359
+ * The numbered list is desktop-only; narrow screens get this
360
+ * single indicator instead so the control never wraps into a
361
+ * second row of buttons.
362
+ */}
363
+ <li className="flex sm:hidden">
364
+ <span
365
+ data-testid="pagination-current-page-indicator"
366
+ className={`${pageButtonBaseClassName} bg-white text-gray-600`}
231
367
  >
232
- <span>Next</span>
233
- </button>
368
+ {isHasMoreMode
369
+ ? `Page ${currentPageNumber.toLocaleString()}`
370
+ : `Page ${currentPageNumber.toLocaleString()} of ${totalPageCount.toLocaleString()}`}
371
+ </span>
234
372
  </li>
235
- </ul>
236
- </div>
237
- </div>
238
-
239
- {/* Mobile layout: Navigate button on left, pagination controls on right */}
240
- <div className="md:hidden my-2">
241
- <Button
242
- dataTestId="show-pagination-modal-button-mobile"
243
- className="my-2"
244
- buttonSize={ButtonSize.ExtraSmall}
245
- icon={IconProp.AdjustmentHorizontal}
246
- buttonStyle={ButtonStyleType.ICON_LIGHT}
247
- ariaLabel="Open pagination settings"
248
- onClick={() => {
249
- setShowPaginationModel(true);
250
- }}
251
- />
252
- </div>
253
373
 
254
- <div className="md:hidden">
255
- <div className="inline-flex -space-x-px rounded-md shadow-sm">
256
- <ul className="flex" role="list">
257
- <li className="flex">
258
- <button
259
- type="button"
260
- disabled={isPreviousDisabled}
261
- aria-label="Go to previous page"
262
- onClick={() => {
263
- let currentPageNumber: number = props.currentPageNumber;
374
+ {isHasMoreMode && (
375
+ <li className="hidden sm:flex">
376
+ <span
377
+ data-testid="pagination-current-page-indicator-desktop"
378
+ aria-current="page"
379
+ className={`${pageButtonBaseClassName} z-10 border-indigo-500 bg-indigo-50 text-indigo-600`}
380
+ >
381
+ {`Page ${currentPageNumber.toLocaleString()}`}
382
+ </span>
383
+ </li>
384
+ )}
264
385
 
265
- if (typeof currentPageNumber === "string") {
266
- currentPageNumber = parseInt(currentPageNumber);
267
- }
386
+ {pageWindow.map((item: PageWindowItem) => {
387
+ if (typeof item === "number") {
388
+ return getPageNumberButton(item);
389
+ }
390
+
391
+ return getEllipsis(item);
392
+ })}
268
393
 
269
- if (props.onNavigateToPage && !isPreviousDisabled) {
270
- props.onNavigateToPage(
271
- currentPageNumber - 1,
272
- props.itemsOnPage,
273
- );
274
- }
275
- }}
276
- className={` inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
277
- isPreviousDisabled
278
- ? "bg-gray-100 cursor-not-allowed"
279
- : "hover:bg-gray-50 cursor-pointer"
280
- }`}
281
- >
282
- <span className="page-link">Previous</span>
283
- </button>
284
- </li>
285
- <li className="flex">
286
- <button
287
- type="button"
288
- disabled={isCurrentPageButtonDisabled}
289
- aria-current="page"
290
- aria-label={`Page ${props.currentPageNumber}, current page. Select to jump to another page.`}
291
- data-testid="current-page-link-mobile"
292
- className={` z-10 inline-flex items-center border border-x-0 border-gray-300 hover:bg-gray-50 px-4 py-2 text-sm font-medium text-text-600 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
293
- isCurrentPageButtonDisabled ? "bg-gray-100" : ""
294
- }`}
295
- onClick={() => {
296
- if (!isHasMoreMode) {
297
- setShowPaginationModel(true);
298
- }
299
- }}
300
- >
301
- <span>{props.currentPageNumber}</span>
302
- </button>
303
- </li>
304
394
  <li className="flex">
305
395
  <button
306
396
  type="button"
397
+ data-testid="pagination-next-button"
307
398
  disabled={isNextDisabled}
308
399
  aria-label="Go to next page"
309
400
  onClick={() => {
310
- let currentPageNumber: number = props.currentPageNumber;
311
-
312
- if (typeof currentPageNumber === "string") {
313
- currentPageNumber = parseInt(currentPageNumber);
314
- }
315
-
316
- if (props.onNavigateToPage && !isNextDisabled) {
317
- props.onNavigateToPage(
318
- currentPageNumber + 1,
319
- props.itemsOnPage,
320
- );
401
+ if (!isNextDisabled) {
402
+ navigateToPage(currentPageNumber + 1);
321
403
  }
322
404
  }}
323
- className={` inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 ${
324
- isNextDisabled
325
- ? "bg-gray-100 cursor-not-allowed"
326
- : " hover:bg-gray-50 cursor-pointer"
327
- }`}
405
+ className={`${getArrowButtonClassName(
406
+ isNextDisabled,
407
+ )} rounded-r-md`}
328
408
  >
329
- <span>Next</span>
409
+ <Icon icon={IconProp.ChevronRight} className="h-4 w-4" />
330
410
  </button>
331
411
  </li>
332
412
  </ul>
333
413
  </div>
334
- </div>
414
+ </nav>
335
415
 
336
- {showPaginationModel && (
337
- <BasicFormModal<PaginationNavigationItem>
338
- data-testid="pagination-modal"
339
- title={isHasMoreMode ? "Items on Page" : "Navigate to Page"}
416
+ {isGoToPageModalVisible && (
417
+ <Modal
418
+ title="Go to page"
419
+ description={`This list has ${totalPageCount.toLocaleString()} pages. Enter the one you want to see.`}
420
+ submitButtonText="Go"
421
+ closeButtonText="Cancel"
422
+ disableSubmitButton={goToPageValue === ""}
423
+ onSubmit={submitGoToPage}
340
424
  onClose={() => {
341
- setShowPaginationModel(false);
425
+ setIsGoToPageModalVisible(false);
426
+ setGoToPageValue("");
342
427
  }}
343
- submitButtonText={isHasMoreMode ? "Apply" : "Go to Page"}
344
- onSubmit={(item: PaginationNavigationItem) => {
345
- if (props.onNavigateToPage) {
428
+ >
429
+ <div>
430
+ <label
431
+ htmlFor={goToPageInputId}
432
+ className="block text-sm font-medium text-gray-700"
433
+ >
434
+ Page number
435
+ </label>
436
+ <input
437
+ id={goToPageInputId}
438
+ data-testid="pagination-go-to-page-input"
439
+ type="number"
440
+ inputMode="numeric"
441
+ autoFocus={true}
442
+ min={1}
443
+ max={totalPageCount}
444
+ value={goToPageValue}
445
+ placeholder={`1-${totalPageCount}`}
446
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
447
+ setGoToPageValue(event.target.value);
448
+ }}
346
449
  /*
347
- * hasMore mode doesn't know the max page, so the
348
- * pageNumber field is hidden navigating "in place"
349
- * keeps the current page and only changes the page
350
- * size.
450
+ * The modal's footer button is not a form submit, so Enter is
451
+ * wired up by hand - typing a number and pressing Enter is how
452
+ * anyone who reached for this box expects it to end.
351
453
  */
352
- const pageNumber: number = isHasMoreMode
353
- ? props.currentPageNumber
354
- : item.pageNumber;
355
- props.onNavigateToPage(pageNumber, item.itemsOnPage);
356
- }
357
- setShowPaginationModel(false);
358
- }}
359
- formProps={{
360
- initialValues: {
361
- pageNumber: props.currentPageNumber,
362
- itemsOnPage: props.itemsOnPage,
363
- },
364
- fields: [
365
- ...(isHasMoreMode
366
- ? []
367
- : [
368
- {
369
- title: "Page Number",
370
- description: `You can enter page numbers from ${
371
- minPageNumber !== maxPageNumber
372
- ? minPageNumber + " to " + maxPageNumber
373
- : minPageNumber
374
- }. Please enter it here:`,
375
- field: {
376
- pageNumber: true,
377
- },
378
- disabled: minPageNumber === maxPageNumber,
379
- placeholder: "1",
380
- required: true,
381
- validation: {
382
- minValue: minPageNumber,
383
- maxValue: maxPageNumber,
384
- },
385
- fieldType: FormFieldSchemaType.PositiveNumber,
386
- },
387
- ]),
388
- {
389
- title: `${props.pluralLabel} on Page `,
390
- description: `Enter the number of ${props.pluralLabel.toLowerCase()} you would like to see on the page:`,
391
- field: {
392
- itemsOnPage: true,
393
- },
394
- placeholder: "10",
395
- required: true,
396
- fieldType: FormFieldSchemaType.Dropdown,
397
- dropdownOptions: [
398
- {
399
- value: 10,
400
- label: "10",
401
- },
402
- {
403
- value: 20,
404
- label: "20",
405
- },
406
- {
407
- value: 25,
408
- label: "25",
409
- },
410
- {
411
- value: 50,
412
- label: "50",
413
- },
414
- ],
415
- },
416
- ],
417
- }}
418
- />
454
+ onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
455
+ if (event.key === "Enter") {
456
+ event.preventDefault();
457
+ submitGoToPage();
458
+ }
459
+ }}
460
+ className="mt-2 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm placeholder:text-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
461
+ />
462
+ </div>
463
+ </Modal>
419
464
  )}
420
- </nav>
465
+ </>
421
466
  );
422
467
  };
423
468