From 3dbfe0d2e38b5336eaf621cd939378977fa4c588 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 4 Dec 2025 12:22:22 -0500 Subject: [PATCH 1/5] UID-208 display mod-scheduler timers Display timers from mod-scheduler when available. Refs UID-208 --- CHANGELOG.md | 1 + package.json | 8 +++ src/hooks/useSchedulerTimers.js | 29 ++++++++ src/settings/SchedulerTimers.js | 116 ++++++++++++++++++++++++++++++ src/settings/index.js | 43 +++++++++-- translations/ui-developer/en.json | 9 +++ 6 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useSchedulerTimers.js create mode 100644 src/settings/SchedulerTimers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cba5ff7..17cad73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Session timezone support. Refs UID-204. * Tolerate missing `name` attribute when sorting module descriptors. Refs UID-200. * Remove display of mod-configuration values. Refs UID-182. +* Display mod-scheduler's timers. Refs UID-208. ## [10.0.0](https://github.com/folio-org/ui-developer/tree/v10.0.0) (2025-03-17) [Full Changelog](https://github.com/folio-org/ui-developer/compare/v9.0.0...v10.0.0) diff --git a/package.json b/package.json index bc75c9b7..7889820a 100644 --- a/package.json +++ b/package.json @@ -210,6 +210,14 @@ ], "visible": true }, + { + "permissionName": "ui-developer.settings.schedulerTimers", + "displayName": "Settings (developer): Can view mod-scheduler timers", + "subPermissions": [ + "settings.developer.enabled" + ], + "visible": true + }, { "permissionName": "ui-developer.settings.app-manager", "displayName": "Settings (developer): Can use the app manager", diff --git a/src/hooks/useSchedulerTimers.js b/src/hooks/useSchedulerTimers.js new file mode 100644 index 00000000..b8ef0ca3 --- /dev/null +++ b/src/hooks/useSchedulerTimers.js @@ -0,0 +1,29 @@ +import { + useQuery, +} from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +const useSchedulerTimers = () => { + const [namespace] = useNamespace(); + const ky = useOkapiKy(); + + const searchParams = { + limit: 500, + }; + + const { data, isLoading } = useQuery( + { + queryKey: [namespace, 'schedulerTimers'], + queryFn: () => ky.get('scheduler/timers', { searchParams }) + .then(response => response.json()), + }, + ); + + return { data, isLoading }; +}; + +export default useSchedulerTimers; diff --git a/src/settings/SchedulerTimers.js b/src/settings/SchedulerTimers.js new file mode 100644 index 00000000..3e6afb1d --- /dev/null +++ b/src/settings/SchedulerTimers.js @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { + LoadingPane, + MultiColumnList, + NoValue, + Pane, + Row, +} from '@folio/stripes/components'; + +import useSchedulerTimers from '../hooks/useSchedulerTimers'; + +const comparators = { + type: (a, b) => a.type.localeCompare(b.type), + moduleName: (a, b) => a.moduleName.localeCompare(b.moduleName), + path: (a, b) => a.routingEntry.pathPattern.localeCompare(b.routingEntry.pathPattern), + unit: (a, b) => a.routingEntry.unit?.localeCompare(b.routingEntry.unit), + delay: (a, b) => { + const aInt = Number.parseInt(a.routingEntry.delay, 10) || 0; + const bInt = Number.parseInt(b.routingEntry.delay, 10) || 0; + return aInt - bInt; + }, + method: (a, b) => { + const aMethods = a.routingEntry.methods.join(','); + const bMethods = b.routingEntry.methods.join(','); + + return aMethods.localeCompare(bMethods); + }, + schedule: (a, b) => { + const aSchedule = a.routingEntry.schedule?.cron || ''; + const bSchedule = b.routingEntry.schedule?.cron || ''; + + return aSchedule.localeCompare(bSchedule); + }, +}; + +const SchedulerTimers = () => { + const { data, isLoading } = useSchedulerTimers(); + const [dataSort, setDataSort] = useState({ field: 'moduleName', direction: 'ascending' }); + + const columnMapping = { + delay: , + id: , + method: , + path: , + unit: , + schedule: , + }; + + const formatter = { + type: o => o.type, + moduleName: o => o.moduleName, + method: o => o.routingEntry.methods.join(', '), + path: o => o.routingEntry.pathPattern, + unit: o => o.routingEntry.unit ?? , + delay: o => (o.routingEntry.delay ? : ), + schedule: o => (o.routingEntry.schedule ? {o.routingEntry.schedule.cron}, {o.routingEntry.schedule.zone} : ), + }; + + const onHeaderClick = (_e, m) => { + setDataSort(prevState => { + if (prevState.field === m.name) { + return { + field: m.name, + direction: prevState.direction === 'ascending' ? 'descending' : 'ascending', + }; + } + + return { + field: m.name, + direction: 'ascending', + }; + }); + }; + + if (isLoading) return ; + + const sortedData = () => { + const list = data.timerDescriptors.toSorted(comparators[dataSort.field]); + return dataSort.direction === 'ascending' ? list : list.reverse(); + }; + + return ( + } + > + + + + + ); +}; + +SchedulerTimers.propTypes = { + stripes: PropTypes.shape({ + okapi: PropTypes.shape({ + tenant: PropTypes.string, + }) + }).isRequired, +}; + +export default SchedulerTimers; diff --git a/src/settings/index.js b/src/settings/index.js index f5f361c1..0c321fed 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -22,11 +22,20 @@ import PermissionsInspector from './PermissionsInspector'; import OkapiConsole from './OkapiConsole'; import UserLocale from './UserLocale'; import OkapiTimers from './OkapiTimers'; +import SchedulerTimers from './SchedulerTimers'; import AppManager from './AppManager'; import RefreshTokenRotation from './RefreshTokenRotation'; import ShowCapabilities from './ShowCapabilities'; +import Test from './Test'; +import Bork from './Bork'; const pages = [ + { + route: 'bork', + labelId: 'ui-developer.bork', + component: Bork, + // perm: 'ui-developer.settings.configuration', + }, { route: 'configuration', labelId: 'ui-developer.configuration', @@ -115,19 +124,17 @@ const pages = [ component: UserLocale, perm: 'ui-developer.settings.userLocale', }, - { - route: 'okapi-timers', - labelId: 'ui-developer.okapiTimers', - component: OkapiTimers, - perm: 'ui-developer.settings.okapiTimers', - }, { route: 'rtr', labelId: 'ui-developer.rtr', component: RefreshTokenRotation, perm: 'ui-developer.settings.rtr', }, - + { + route: 'test', + labelId: 'ui-developer.test-playground', + component: Test, + }, ]; const DeveloperSettings = (props) => { @@ -166,6 +173,28 @@ const DeveloperSettings = (props) => { }); } + if (stripes.hasInterface('okapi')) { + allPages.push( + { + route: 'okapi-timers', + labelId: 'ui-developer.okapiTimers', + component: OkapiTimers, + perm: 'ui-developer.settings.okapiTimers', + }, + ); + } + + if (stripes.hasInterface('scheduler')) { + allPages.push( + { + route: 'scheduler-timers', + labelId: 'ui-developer.schedulerTimers', + component: SchedulerTimers, + perm: 'ui-developer.settings.schedulerTimers', + } + ); + } + allPages.forEach(p => { p.label = intl.formatMessage({ id: p.labelId }); }); diff --git a/translations/ui-developer/en.json b/translations/ui-developer/en.json index 03ca2b1e..1d8b302d 100644 --- a/translations/ui-developer/en.json +++ b/translations/ui-developer/en.json @@ -134,6 +134,15 @@ "okapiTimers.unit": "Unit", "okapiTimers.delay": "Delay", + + "schedulerTimers": "mod-scheduler timers", + "schedulerTimers.id": "ID", + "schedulerTimers.method": "Method", + "schedulerTimers.path": "Path", + "schedulerTimers.unit": "Unit", + "schedulerTimers.delay": "Delay", + "schedulerTimers.schedule": "Schedule", + "app-manager": "App manager", "app-manager.apps": "Applications", "app-manager.apps.count": "{count} {count, plural, one {application} other {applications}}", From f65e2e5949a74eab7a1cffb7102e05c3a5f5a484 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 4 Dec 2025 12:25:48 -0500 Subject: [PATCH 2/5] UID-208 display mod-scheduler timers Display mod-scheduler timers with URL updates via nuqs. Refs UID-208 --- package.json | 1 + src/hooks/useNuqsAdaptor.js | 42 +++++++++++++++++++++ src/settings/SchedulerTimers.js | 65 ++++++++++++++++++--------------- src/settings/index.js | 11 +++++- 4 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 src/hooks/useNuqsAdaptor.js diff --git a/package.json b/package.json index 7889820a..7c2b24be 100644 --- a/package.json +++ b/package.json @@ -266,6 +266,7 @@ "ky": "^0.23.0", "localforage": "^1.10.0", "lodash": "^4.17.4", + "nuqs": "^2.8.2", "prop-types": "^15.6.0", "react-chartjs-2": "^5.2.0", "react-inspector": "^6.0.0", diff --git a/src/hooks/useNuqsAdaptor.js b/src/hooks/useNuqsAdaptor.js new file mode 100644 index 00000000..1c0cef68 --- /dev/null +++ b/src/hooks/useNuqsAdaptor.js @@ -0,0 +1,42 @@ +import { + unstable_createAdapterProvider as createAdapterProvider, + renderQueryString, +} from 'nuqs/adapters/custom'; +import { useCallback, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +function useNuqsReactRouterV5Adapter() { + const history = useHistory(); + const location = useLocation(); + const searchParams = useMemo(() => { + return new URLSearchParams(location.search); + }, [location.search]); + + const updateUrl = useCallback( + (search, options) => { + const queryString = renderQueryString(search); + if (options.history === 'push') { + history.push({ + search: queryString, + hash: window.location.hash + }); + } else { + history.replace({ + search: queryString, + hash: window.location.hash + }); + } + if (options.scroll) { + window.scrollTo(0, 0); + } + }, + [history.push, history.replace] + ); + + return { + searchParams, + updateUrl + }; +} + +export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV5Adapter); diff --git a/src/settings/SchedulerTimers.js b/src/settings/SchedulerTimers.js index 3e6afb1d..e93e94ab 100644 --- a/src/settings/SchedulerTimers.js +++ b/src/settings/SchedulerTimers.js @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, FormattedNumber } from 'react-intl'; +import { useQueryState, parseAsString, parseAsStringLiteral } from 'nuqs'; import { LoadingPane, @@ -13,75 +14,79 @@ import { import useSchedulerTimers from '../hooks/useSchedulerTimers'; const comparators = { - type: (a, b) => a.type.localeCompare(b.type), - moduleName: (a, b) => a.moduleName.localeCompare(b.moduleName), - path: (a, b) => a.routingEntry.pathPattern.localeCompare(b.routingEntry.pathPattern), - unit: (a, b) => a.routingEntry.unit?.localeCompare(b.routingEntry.unit), delay: (a, b) => { const aInt = Number.parseInt(a.routingEntry.delay, 10) || 0; const bInt = Number.parseInt(b.routingEntry.delay, 10) || 0; return aInt - bInt; }, + id: (a, b) => a.id.localeCompare(b.id), method: (a, b) => { const aMethods = a.routingEntry.methods.join(','); const bMethods = b.routingEntry.methods.join(','); return aMethods.localeCompare(bMethods); }, + // moduleName: (a, b) => a.moduleName.localeCompare(b.moduleName), + path: (a, b) => a.routingEntry.pathPattern.localeCompare(b.routingEntry.pathPattern), schedule: (a, b) => { const aSchedule = a.routingEntry.schedule?.cron || ''; const bSchedule = b.routingEntry.schedule?.cron || ''; return aSchedule.localeCompare(bSchedule); }, + type: (a, b) => a.type.localeCompare(b.type), + unit: (a, b) => a.routingEntry.unit?.localeCompare(b.routingEntry.unit), }; const SchedulerTimers = () => { const { data, isLoading } = useSchedulerTimers(); - const [dataSort, setDataSort] = useState({ field: 'moduleName', direction: 'ascending' }); + const [sortField, setSortField] = useQueryState('sortField', + parseAsString.withOptions({ + defaultValue: 'moduleName', + history: 'push' + })); + const [sortDirection, setSortDirection] = useQueryState('sortDirection', + parseAsStringLiteral(['asc', 'desc']).withOptions({ + defaultValue: 'asc', + history: 'push' + })); const columnMapping = { delay: , id: , method: , + moduleName: , path: , - unit: , schedule: , + type: , + unit: , }; const formatter = { - type: o => o.type, - moduleName: o => o.moduleName, + delay: o => (o.routingEntry.delay ? : ), method: o => o.routingEntry.methods.join(', '), path: o => o.routingEntry.pathPattern, - unit: o => o.routingEntry.unit ?? , - delay: o => (o.routingEntry.delay ? : ), schedule: o => (o.routingEntry.schedule ? {o.routingEntry.schedule.cron}, {o.routingEntry.schedule.zone} : ), + unit: o => o.routingEntry.unit ?? , }; const onHeaderClick = (_e, m) => { - setDataSort(prevState => { - if (prevState.field === m.name) { - return { - field: m.name, - direction: prevState.direction === 'ascending' ? 'descending' : 'ascending', - }; + setSortField(m.name); + setSortDirection(prevState => { + if (sortField === m.name) { + return prevState === 'asc' ? 'desc' : 'asc'; } - - return { - field: m.name, - direction: 'ascending', - }; + return 'asc'; }); }; - if (isLoading) return ; - const sortedData = () => { - const list = data.timerDescriptors.toSorted(comparators[dataSort.field]); - return dataSort.direction === 'ascending' ? list : list.reverse(); + const list = data.timerDescriptors.toSorted(comparators[sortField]); + return sortDirection === 'asc' ? list : list.reverse(); }; + if (isLoading) return ; + return ( { columnMapping={columnMapping} contentData={sortedData()} formatter={formatter} - visibleColumns={['moduleName', 'type', 'unit', 'delay', 'schedule', 'method', 'path']} + visibleColumns={['id', 'moduleName', 'type', 'unit', 'delay', 'schedule', 'method', 'path']} showSortIndicator - sortableFields={['moduleName', 'type', 'unit', 'delay', 'schedule', 'method', 'path']} + sortableFields={['id', 'moduleName', 'type', 'unit', 'delay', 'schedule', 'method', 'path']} onHeaderClick={onHeaderClick} interactive={false} - sortDirection={dataSort.direction} - sortedColumn={dataSort.field} + sortDirection={sortDirection} + sortedColumn={sortField} /> diff --git a/src/settings/index.js b/src/settings/index.js index 0c321fed..9283b9b6 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -1,9 +1,12 @@ import React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; +import { BrowserRouter } from 'react-router-dom'; import { useStripes } from '@folio/stripes/core'; import { Settings } from '@folio/stripes/smart-components'; +import { NuqsAdapter } from '../hooks/useNuqsAdaptor'; + import Configuration from './Configuration'; import ShowPermissions from './ShowPermissions'; import SessionLocale from './SessionLocale'; @@ -203,7 +206,13 @@ const DeveloperSettings = (props) => { return a.label.localeCompare(b.label); }); - return } />; + return ( + + + } /> + + + ); }; export default DeveloperSettings; From 2d84b1c13880b30c85a5e675201cae58bb70d75b Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Fri, 5 Dec 2025 15:49:42 -0500 Subject: [PATCH 3/5] don't include references to dummy test files ya big dummy --- src/settings/index.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/settings/index.js b/src/settings/index.js index 9283b9b6..41454aa4 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -29,17 +29,9 @@ import SchedulerTimers from './SchedulerTimers'; import AppManager from './AppManager'; import RefreshTokenRotation from './RefreshTokenRotation'; import ShowCapabilities from './ShowCapabilities'; -import Test from './Test'; -import Bork from './Bork'; const pages = [ - { - route: 'bork', - labelId: 'ui-developer.bork', - component: Bork, - // perm: 'ui-developer.settings.configuration', - }, - { + g { route: 'configuration', labelId: 'ui-developer.configuration', component: Configuration, @@ -133,11 +125,6 @@ const pages = [ component: RefreshTokenRotation, perm: 'ui-developer.settings.rtr', }, - { - route: 'test', - labelId: 'ui-developer.test-playground', - component: Test, - }, ]; const DeveloperSettings = (props) => { From 9a607bf5a074fe448e5430f0a9cf2380a8b470e8 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Fri, 5 Dec 2025 15:54:55 -0500 Subject: [PATCH 4/5] no extra characters either --- src/settings/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/index.js b/src/settings/index.js index 41454aa4..590fae39 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -31,7 +31,7 @@ import RefreshTokenRotation from './RefreshTokenRotation'; import ShowCapabilities from './ShowCapabilities'; const pages = [ - g { + { route: 'configuration', labelId: 'ui-developer.configuration', component: Configuration, From c2c4c099c37c15020dfb88b5cdfe2c7dc862e9e2 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 9 Dec 2025 08:18:01 -0500 Subject: [PATCH 5/5] MCL expects ascending/descending --- src/settings/SchedulerTimers.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/settings/SchedulerTimers.js b/src/settings/SchedulerTimers.js index e93e94ab..973c0796 100644 --- a/src/settings/SchedulerTimers.js +++ b/src/settings/SchedulerTimers.js @@ -46,8 +46,8 @@ const SchedulerTimers = () => { history: 'push' })); const [sortDirection, setSortDirection] = useQueryState('sortDirection', - parseAsStringLiteral(['asc', 'desc']).withOptions({ - defaultValue: 'asc', + parseAsStringLiteral(['ascending', 'descending']).withOptions({ + defaultValue: 'ascending', history: 'push' })); @@ -74,15 +74,15 @@ const SchedulerTimers = () => { setSortField(m.name); setSortDirection(prevState => { if (sortField === m.name) { - return prevState === 'asc' ? 'desc' : 'asc'; + return prevState === 'ascending' ? 'descending' : 'ascending'; } - return 'asc'; + return 'ascending'; }); }; const sortedData = () => { const list = data.timerDescriptors.toSorted(comparators[sortField]); - return sortDirection === 'asc' ? list : list.reverse(); + return sortDirection === 'ascending' ? list : list.reverse(); }; if (isLoading) return ;