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..7c2b24be 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",
@@ -258,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/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..973c0796
--- /dev/null
+++ b/src/settings/SchedulerTimers.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, FormattedNumber } from 'react-intl';
+import { useQueryState, parseAsString, parseAsStringLiteral } from 'nuqs';
+
+import {
+ LoadingPane,
+ MultiColumnList,
+ NoValue,
+ Pane,
+ Row,
+} from '@folio/stripes/components';
+
+import useSchedulerTimers from '../hooks/useSchedulerTimers';
+
+const comparators = {
+ 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 [sortField, setSortField] = useQueryState('sortField',
+ parseAsString.withOptions({
+ defaultValue: 'moduleName',
+ history: 'push'
+ }));
+ const [sortDirection, setSortDirection] = useQueryState('sortDirection',
+ parseAsStringLiteral(['ascending', 'descending']).withOptions({
+ defaultValue: 'ascending',
+ history: 'push'
+ }));
+
+ const columnMapping = {
+ delay: ,
+ id: ,
+ method: ,
+ moduleName: ,
+ path: ,
+ schedule: ,
+ type: ,
+ unit: ,
+ };
+
+ const formatter = {
+ delay: o => (o.routingEntry.delay ? : ),
+ method: o => o.routingEntry.methods.join(', '),
+ path: o => o.routingEntry.pathPattern,
+ schedule: o => (o.routingEntry.schedule ? {o.routingEntry.schedule.cron}, {o.routingEntry.schedule.zone} : ),
+ unit: o => o.routingEntry.unit ?? ,
+ };
+
+ const onHeaderClick = (_e, m) => {
+ setSortField(m.name);
+ setSortDirection(prevState => {
+ if (sortField === m.name) {
+ return prevState === 'ascending' ? 'descending' : 'ascending';
+ }
+ return 'ascending';
+ });
+ };
+
+ const sortedData = () => {
+ const list = data.timerDescriptors.toSorted(comparators[sortField]);
+ return sortDirection === 'ascending' ? list : list.reverse();
+ };
+
+ if (isLoading) return ;
+
+ 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..590fae39 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';
@@ -22,6 +25,7 @@ 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';
@@ -115,19 +119,12 @@ 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',
},
-
];
const DeveloperSettings = (props) => {
@@ -166,6 +163,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 });
});
@@ -174,7 +193,13 @@ const DeveloperSettings = (props) => {
return a.label.localeCompare(b.label);
});
- return } />;
+ return (
+
+
+ } />
+
+
+ );
};
export default DeveloperSettings;
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}}",