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}}",