diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml index 56b4e1cd4c6..7baa1f04429 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml @@ -80,6 +80,10 @@ org.jolokia jolokia-support-springboot + + org.springframework.boot + spring-boot-starter-quartz + org.springframework.boot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/QuartzJobsConfiguration.java b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/QuartzJobsConfiguration.java new file mode 100644 index 00000000000..bc103136749 --- /dev/null +++ b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/QuartzJobsConfiguration.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.codecentric.boot.admin.sample; + +import java.util.TimeZone; + +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.quartz.QuartzJobBean; + +/** + * Configuration for sample Quartz jobs and triggers to demonstrate the Quartz actuator + * endpoint. This allows testing of job/trigger listing in Spring Boot Admin UI. + */ +@Configuration +public class QuartzJobsConfiguration { + + /** + * Creates job details for the sample job. + * @return job detail for the sample job + */ + @Bean + public JobDetail sampleJobDetail() { + return JobBuilder.newJob(SampleJob.class) + .withIdentity("sampleJob", "samples") + .withDescription("Sample job to demonstrate Quartz actuator endpoint") + .storeDurably() + .build(); + } + + /** + * Creates job details for the another sample job. + * @return job detail for the another sample job + */ + @Bean + public JobDetail anotherSampleJobDetail() { + return JobBuilder.newJob(AnotherSampleJob.class) + .withIdentity("anotherJob", "samples") + .withDescription("Another sample job for testing") + .storeDurably() + .build(); + } + + /** + * Creates a simple trigger that executes the sample job every 10 seconds. + * @return trigger for the sample job + */ + @Bean + public Trigger sampleJobTrigger() { + return TriggerBuilder.newTrigger() + .forJob(sampleJobDetail()) + .withIdentity("sampleTrigger", "samples") + .withDescription("Trigger that executes sample job every 10 seconds") + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(10).repeatForever()) + .build(); + } + + /** + * Creates a cron trigger that executes another sample job every day at 3am. + * @return trigger for the another sample job + */ + @Bean + public Trigger anotherSampleJobTrigger() { + return TriggerBuilder.newTrigger() + .forJob(anotherSampleJobDetail()) + .withIdentity("dailyTrigger", "samples") + .withDescription("Daily trigger at 3am") + .withSchedule(CronScheduleBuilder.cronSchedule("0 0 3 * * ?").inTimeZone(TimeZone.getTimeZone("UTC"))) + .build(); + } + + /** + * Creates a simple trigger for testing purposes (every hour). + * @return trigger for hourly execution + */ + @Bean + public Trigger hourlyTestTrigger() { + return TriggerBuilder.newTrigger() + .forJob(sampleJobDetail()) + .withIdentity("hourlyTrigger", "DEFAULT") + .withDescription("Hourly trigger for testing") + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1).repeatForever()) + .build(); + } + + /** + * Sample job that logs at regular intervals. + */ + public static class SampleJob extends QuartzJobBean { + + @Override + protected void executeInternal(org.quartz.JobExecutionContext context) { + System.out.println("Sample Quartz Job executed at " + new java.util.Date()); + } + + } + + /** + * Another sample job for demonstration. + */ + public static class AnotherSampleJob extends QuartzJobBean { + + @Override + protected void executeInternal(org.quartz.JobExecutionContext context) { + System.out.println("Another Quartz Job executed at " + new java.util.Date()); + } + + } + +} diff --git a/spring-boot-admin-server-ui/src/main/frontend/components/font-awesome-icon.ts b/spring-boot-admin-server-ui/src/main/frontend/components/font-awesome-icon.ts index 7e424d2890c..879a10c6cb0 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/components/font-awesome-icon.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/components/font-awesome-icon.ts @@ -21,6 +21,8 @@ import { faStopCircle as farStopCircle } from '@fortawesome/free-regular-svg-ico import { faTimesCircle as farTimesCircle } from '@fortawesome/free-regular-svg-icons/faTimesCircle'; import { faAngleDoubleLeft, + faBriefcase, + faClock, faCogs, faExpand, faEye, @@ -34,6 +36,8 @@ import { faBars } from '@fortawesome/free-solid-svg-icons/faBars'; import { faBell } from '@fortawesome/free-solid-svg-icons/faBell'; import { faBellSlash } from '@fortawesome/free-solid-svg-icons/faBellSlash'; import { faBook } from '@fortawesome/free-solid-svg-icons/faBook'; +import { faCalendar } from '@fortawesome/free-solid-svg-icons/faCalendar'; +import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons/faCalendarCheck'; import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'; import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle'; import { faChevronDown } from '@fortawesome/free-solid-svg-icons/faChevronDown'; @@ -49,13 +53,18 @@ import { faFrownOpen } from '@fortawesome/free-solid-svg-icons/faFrownOpen'; import { faHeartbeat } from '@fortawesome/free-solid-svg-icons/faHeartbeat'; import { faHistory } from '@fortawesome/free-solid-svg-icons/faHistory'; import { faHome } from '@fortawesome/free-solid-svg-icons/faHome'; +import { faHourglass } from '@fortawesome/free-solid-svg-icons/faHourglass'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'; +import { faLink } from '@fortawesome/free-solid-svg-icons/faLink'; import { faMapMarkerAlt } from '@fortawesome/free-solid-svg-icons/faMapMarkerAlt'; import { faMinusCircle } from '@fortawesome/free-solid-svg-icons/faMinusCircle'; +import { faPauseCircle } from '@fortawesome/free-solid-svg-icons/faPauseCircle'; import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt'; +import { faPlayCircle } from '@fortawesome/free-solid-svg-icons/faPlayCircle'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons/faQuestionCircle'; import { faRedo } from '@fortawesome/free-solid-svg-icons/faRedo'; import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch'; +import { faShield } from '@fortawesome/free-solid-svg-icons/faShield'; import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt'; import { faStepBackward } from '@fortawesome/free-solid-svg-icons/faStepBackward'; import { faStepForward } from '@fortawesome/free-solid-svg-icons/faStepForward'; @@ -77,6 +86,10 @@ library.add( faBellSlash, faBook, faBars, + faBriefcase, + faCalendar, + faCalendarCheck, + faClock, faCogs, faEye, faCheck, @@ -90,9 +103,11 @@ library.add( faFrownOpen, faHeartbeat, faHistory, + faHourglass, faInfoCircle, faExclamationCircle, faHome, + faLink, faList, faExpand, faMapMarkerAlt, @@ -100,10 +115,13 @@ library.add( faMinusCircle, faChevronRight, faChevronDown, + faPauseCircle, faPencilAlt, + faPlayCircle, faPowerOff, faQuestionCircle, faSearch, + faShield, faSignOutAlt, faStepBackward, faStepForward, diff --git a/spring-boot-admin-server-ui/src/main/frontend/login.css b/spring-boot-admin-server-ui/src/main/frontend/login.css index f6659092dec..6d8c3018452 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/login.css +++ b/spring-boot-admin-server-ui/src/main/frontend/login.css @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "./theme.css"; +@import './theme.css'; :root { --bg-color-start: #71e69c; diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts index 44de1753031..a2590b86be8 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ const isInstanceActuatorRequest = (url: string) => class Instance { public readonly id: string; - private readonly axios: AxiosInstance; + readonly axios: AxiosInstance; public registration: Registration; public endpoints: Endpoint[] = []; public availableMetrics: string[] = []; @@ -481,30 +481,6 @@ class Instance { return this.axios.get(uri`actuator/mappings`); } - async fetchQuartzJobs() { - return this.axios.get(uri`actuator/quartz/jobs`, { - headers: { Accept: 'application/json' }, - }); - } - - async fetchQuartzJob(group, name) { - return this.axios.get(uri`actuator/quartz/jobs/${group}/${name}`, { - headers: { Accept: 'application/json' }, - }); - } - - async fetchQuartzTriggers() { - return this.axios.get(uri`actuator/quartz/triggers`, { - headers: { Accept: 'application/json' }, - }); - } - - async fetchQuartzTrigger(group, name) { - return this.axios.get(uri`actuator/quartz/triggers/${group}/${name}`, { - headers: { Accept: 'application/json' }, - }); - } - async fetchSbomIds() { return this.axios.get(uri`actuator/sbom`, { headers: { Accept: 'application/json' }, diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.spec.ts new file mode 100644 index 00000000000..adfd79513b5 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.spec.ts @@ -0,0 +1,717 @@ +/* + * Copyright 2014-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + JobDetail, + QuartzActuatorService, + TriggerDetail, +} from './quartz-actuator'; + +// Mock Instance type +interface MockInstance { + axios: { + get: ReturnType; + post?: ReturnType; + }; +} + +const createMockInstance = (): MockInstance => ({ + axios: { + get: vi.fn(), + }, +}); + +describe('QuartzActuatorService', () => { + let mockInstance: MockInstance; + + beforeEach(() => { + mockInstance = createMockInstance(); + }); + + describe('fetchJobGroups', () => { + it('should fetch job groups from correct endpoint', async () => { + const mockResponse = { + data: { + groups: { + samples: { jobs: ['job1', 'job2'] }, + tests: { jobs: ['job3'] }, + }, + }, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobGroups( + mockInstance as any, + ); + + expect(mockInstance.axios.get).toHaveBeenCalledWith( + expect.stringContaining('actuator/quartz/jobs'), + { headers: { Accept: 'application/json' } }, + ); + expect(result.data).toEqual(mockResponse.data); + }); + + it('should handle empty job groups', async () => { + const mockResponse = { data: { groups: {} } }; + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobGroups( + mockInstance as any, + ); + + expect(result.data.groups).toEqual({}); + }); + + it('should propagate errors from axios', async () => { + const error = new Error('Network error'); + mockInstance.axios.get.mockRejectedValue(error); + + await expect( + QuartzActuatorService.fetchJobGroups(mockInstance as any), + ).rejects.toThrow('Network error'); + }); + }); + + describe('fetchJobDetail', () => { + it('should fetch job detail with correct path parameters', async () => { + const mockResponse = { + data: { + group: 'samples', + name: 'jobOne', + className: 'org.springframework.scheduling.quartz.DelegatingJob', + durable: false, + requestRecovery: false, + triggers: [], + } as JobDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobDetail( + mockInstance as any, + 'samples', + 'jobOne', + ); + + expect(mockInstance.axios.get).toHaveBeenCalledWith( + expect.stringContaining('actuator/quartz/jobs/samples/jobOne'), + { headers: { Accept: 'application/json' } }, + ); + expect(result.data.name).toBe('jobOne'); + }); + + it('should include job description when present', async () => { + const mockResponse = { + data: { + group: 'samples', + name: 'jobOne', + description: 'A sample job', + className: 'org.springframework.scheduling.quartz.DelegatingJob', + durable: false, + requestRecovery: false, + triggers: [], + } as JobDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobDetail( + mockInstance as any, + 'samples', + 'jobOne', + ); + + expect(result.data.description).toBe('A sample job'); + }); + + it('should include job data map when present', async () => { + const mockResponse = { + data: { + group: 'samples', + name: 'jobOne', + className: 'org.springframework.scheduling.quartz.DelegatingJob', + durable: false, + requestRecovery: false, + data: { username: 'admin', apiKey: 'secret' }, + triggers: [], + } as JobDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobDetail( + mockInstance as any, + 'samples', + 'jobOne', + ); + + expect(result.data.data).toEqual({ + username: 'admin', + apiKey: 'secret', + }); + }); + + it('should include associated triggers', async () => { + const mockResponse = { + data: { + group: 'samples', + name: 'jobOne', + className: 'org.springframework.scheduling.quartz.DelegatingJob', + durable: false, + requestRecovery: false, + triggers: [ + { + group: 'samples', + name: 'every-day', + nextFireTime: '2020-12-04T12:00:00.000Z', + priority: 7, + }, + ], + } as JobDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchJobDetail( + mockInstance as any, + 'samples', + 'jobOne', + ); + + expect(result.data.triggers).toHaveLength(1); + expect(result.data.triggers[0].name).toBe('every-day'); + }); + }); + + describe('fetchAllJobs', () => { + it('should fetch and aggregate all jobs from all groups', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { jobs: ['job1', 'job2'] }, + }, + }, + }; + + const mockJob1 = { + data: { + name: 'job1', + group: 'samples', + className: 'Job1', + durable: false, + requestRecovery: false, + triggers: [], + } as JobDetail, + }; + + const mockJob2 = { + data: { + name: 'job2', + group: 'samples', + className: 'Job2', + durable: false, + requestRecovery: false, + triggers: [], + } as JobDetail, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValueOnce(mockJob1) + .mockResolvedValueOnce(mockJob2); + + const result = await QuartzActuatorService.fetchAllJobs( + mockInstance as any, + ); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual(mockJob1.data); + expect(result[1]).toEqual(mockJob2.data); + }); + + it('should handle jobs from multiple groups', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { jobs: ['job1'] }, + tests: { jobs: ['job2', 'job3'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValueOnce({ data: { name: 'job1', group: 'samples' } }) + .mockResolvedValueOnce({ data: { name: 'job2', group: 'tests' } }) + .mockResolvedValueOnce({ data: { name: 'job3', group: 'tests' } }); + + const result = await QuartzActuatorService.fetchAllJobs( + mockInstance as any, + ); + + expect(result).toHaveLength(3); + }); + + it('should handle partial failures gracefully', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { jobs: ['job1', 'job2'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValueOnce({ + data: { + name: 'job1', + group: 'samples', + className: 'Job1', + durable: false, + requestRecovery: false, + triggers: [], + }, + }) + .mockRejectedValueOnce(new Error('Failed to fetch job2')); + + const result = await QuartzActuatorService.fetchAllJobs( + mockInstance as any, + ); + + // Should return only successful job + expect(result).toHaveLength(1); + expect(result[0].name).toBe('job1'); + }); + + it('should return empty array when no jobs exist', async () => { + const mockGroupsResponse = { + data: { + groups: {}, + }, + }; + + mockInstance.axios.get.mockResolvedValueOnce(mockGroupsResponse); + + const result = await QuartzActuatorService.fetchAllJobs( + mockInstance as any, + ); + + expect(result).toHaveLength(0); + }); + + it('should make correct number of API calls', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { jobs: ['job1', 'job2'] }, + tests: { jobs: ['job3'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValue({ + data: { + className: 'Job', + durable: false, + requestRecovery: false, + triggers: [], + }, + }); + + await QuartzActuatorService.fetchAllJobs(mockInstance as any); + + // 1 call for groups + 3 calls for individual jobs = 4 total + expect(mockInstance.axios.get).toHaveBeenCalledTimes(4); + }); + }); + + describe('fetchTriggerGroups', () => { + it('should fetch trigger groups from correct endpoint', async () => { + const mockResponse = { + data: { + groups: { + samples: { paused: false, triggers: ['trigger1', 'trigger2'] }, + DEFAULT: { paused: false, triggers: ['trigger3'] }, + }, + }, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerGroups( + mockInstance as any, + ); + + expect(mockInstance.axios.get).toHaveBeenCalledWith( + expect.stringContaining('actuator/quartz/triggers'), + { headers: { Accept: 'application/json' } }, + ); + expect(result.data).toEqual(mockResponse.data); + }); + + it('should include paused status for each group', async () => { + const mockResponse = { + data: { + groups: { + paused_group: { paused: true, triggers: ['trigger1'] }, + active_group: { paused: false, triggers: ['trigger2'] }, + }, + }, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerGroups( + mockInstance as any, + ); + + expect(result.data.groups['paused_group'].paused).toBe(true); + expect(result.data.groups['active_group'].paused).toBe(false); + }); + }); + + describe('fetchTriggerDetail', () => { + it('should fetch trigger detail with correct path parameters', async () => { + const mockResponse = { + data: { + name: 'every-day', + group: 'samples', + type: 'simple' as const, + state: 'NORMAL', + priority: 7, + simple: { interval: 86400000, repeatCount: -1, timesTriggered: 0 }, + } as TriggerDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerDetail( + mockInstance as any, + 'samples', + 'every-day', + ); + + expect(mockInstance.axios.get).toHaveBeenCalledWith( + expect.stringContaining('actuator/quartz/triggers/samples/every-day'), + { headers: { Accept: 'application/json' } }, + ); + expect(result.data.name).toBe('every-day'); + }); + + it('should handle cron triggers with expression and timezone', async () => { + const mockResponse = { + data: { + name: '3am-weekdays', + group: 'samples', + type: 'cron' as const, + state: 'NORMAL', + priority: 3, + cron: { + expression: '0 0 3 ? * 1,2,3,4,5', + timeZone: 'Europe/Paris', + }, + } as TriggerDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerDetail( + mockInstance as any, + 'samples', + '3am-weekdays', + ); + + expect(result.data.cron?.expression).toBe('0 0 3 ? * 1,2,3,4,5'); + expect(result.data.cron?.timeZone).toBe('Europe/Paris'); + }); + + it('should handle daily time interval triggers with days and times', async () => { + const mockResponse = { + data: { + name: 'tue-thu', + group: 'samples', + type: 'dailyTimeInterval' as const, + state: 'NORMAL', + priority: 5, + dailyTimeInterval: { + interval: 3600000, + daysOfWeek: [3, 5], + startTimeOfDay: '09:00:00', + endTimeOfDay: '18:00:00', + repeatCount: -1, + timesTriggered: 0, + }, + } as TriggerDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerDetail( + mockInstance as any, + 'samples', + 'tue-thu', + ); + + expect(result.data.dailyTimeInterval?.daysOfWeek).toEqual([3, 5]); + expect(result.data.dailyTimeInterval?.startTimeOfDay).toBe('09:00:00'); + }); + + it('should handle calendar interval triggers', async () => { + const mockResponse = { + data: { + name: 'once-a-week', + group: 'samples', + type: 'calendarInterval' as const, + state: 'NORMAL', + priority: 5, + calendarInterval: { + interval: 604800000, + timeZone: 'Etc/UTC', + timesTriggered: 0, + preserveHourOfDayAcrossDaylightSavings: false, + skipDayIfHourDoesNotExist: false, + }, + } as TriggerDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerDetail( + mockInstance as any, + 'samples', + 'once-a-week', + ); + + expect(result.data.calendarInterval?.interval).toBe(604800000); + expect(result.data.calendarInterval?.timeZone).toBe('Etc/UTC'); + }); + + it('should handle custom triggers', async () => { + const mockResponse = { + data: { + name: 'custom-trigger', + group: 'samples', + type: 'custom' as const, + state: 'NORMAL', + priority: 10, + custom: { trigger: 'com.example.CustomTrigger@fdsfsd' }, + } as TriggerDetail, + }; + + mockInstance.axios.get.mockResolvedValue(mockResponse); + + const result = await QuartzActuatorService.fetchTriggerDetail( + mockInstance as any, + 'samples', + 'custom-trigger', + ); + + expect(result.data.custom?.trigger).toContain('CustomTrigger'); + }); + }); + + describe('fetchAllTriggers', () => { + it('should fetch and aggregate all triggers from all groups', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { paused: false, triggers: ['trigger1', 'trigger2'] }, + }, + }, + }; + + const mockTrigger1 = { + data: { + name: 'trigger1', + group: 'samples', + type: 'simple' as const, + state: 'NORMAL', + priority: 7, + } as TriggerDetail, + }; + + const mockTrigger2 = { + data: { + name: 'trigger2', + group: 'samples', + type: 'cron' as const, + state: 'NORMAL', + priority: 5, + } as TriggerDetail, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValueOnce(mockTrigger1) + .mockResolvedValueOnce(mockTrigger2); + + const result = await QuartzActuatorService.fetchAllTriggers( + mockInstance as any, + ); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual(mockTrigger1.data); + expect(result[1]).toEqual(mockTrigger2.data); + }); + + it('should handle triggers from multiple groups', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { paused: false, triggers: ['trigger1'] }, + tests: { paused: false, triggers: ['trigger2', 'trigger3'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValue({ + data: { + type: 'simple', + state: 'NORMAL', + priority: 5, + } as TriggerDetail, + }); + + const result = await QuartzActuatorService.fetchAllTriggers( + mockInstance as any, + ); + + expect(result).toHaveLength(3); + }); + + it('should handle partial failures gracefully', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { paused: false, triggers: ['trigger1', 'trigger2'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValueOnce({ + data: { + name: 'trigger1', + group: 'samples', + type: 'simple', + state: 'NORMAL', + priority: 7, + }, + }) + .mockRejectedValueOnce(new Error('Failed to fetch trigger2')); + + const result = await QuartzActuatorService.fetchAllTriggers( + mockInstance as any, + ); + + // Should return only successful trigger + expect(result).toHaveLength(1); + expect(result[0].name).toBe('trigger1'); + }); + + it('should return empty array when no triggers exist', async () => { + const mockGroupsResponse = { + data: { + groups: {}, + }, + }; + + mockInstance.axios.get.mockResolvedValueOnce(mockGroupsResponse); + + const result = await QuartzActuatorService.fetchAllTriggers( + mockInstance as any, + ); + + expect(result).toHaveLength(0); + }); + + it('should handle groups with empty trigger arrays', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { paused: false, triggers: [] }, + }, + }, + }; + + mockInstance.axios.get.mockResolvedValueOnce(mockGroupsResponse); + + const result = await QuartzActuatorService.fetchAllTriggers( + mockInstance as any, + ); + + expect(result).toHaveLength(0); + }); + + it('should make correct number of API calls', async () => { + const mockGroupsResponse = { + data: { + groups: { + samples: { paused: false, triggers: ['t1', 't2'] }, + tests: { paused: false, triggers: ['t3'] }, + }, + }, + }; + + mockInstance.axios.get + .mockResolvedValueOnce(mockGroupsResponse) + .mockResolvedValue({ + data: { + type: 'simple', + state: 'NORMAL', + priority: 5, + } as TriggerDetail, + }); + + await QuartzActuatorService.fetchAllTriggers(mockInstance as any); + + // 1 call for groups + 3 calls for individual triggers = 4 total + expect(mockInstance.axios.get).toHaveBeenCalledTimes(4); + }); + }); + + describe('triggerJob', () => { + it('should post to correct trigger job endpoint with running state', async () => { + mockInstance.axios.post = vi.fn().mockResolvedValue({ + data: { + group: 'samples', + name: 'testJob', + className: 'com.example.TestJob', + triggerTime: '2026-03-26T16:30:00.000Z', + }, + }); + + await QuartzActuatorService.triggerJob( + mockInstance as any, + 'samples', + 'testJob', + ); + + expect(mockInstance.axios.post).toHaveBeenCalledWith( + expect.stringContaining('actuator/quartz/jobs/samples/testJob'), + { state: 'running' }, + ); + }); + }); +}); diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.ts b/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.ts new file mode 100644 index 00000000000..220f9d423d7 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/services/quartz-actuator.ts @@ -0,0 +1,274 @@ +/* + * Copyright 2014-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AxiosResponse } from 'axios'; + +import uri from '../utils/uri'; +import Instance from './instance'; + +/** + * Quartz Actuator Service + * + * Provides static methods to interact with Spring Boot Quartz Actuator endpoint. + * All methods accept an Instance parameter and return promises with typed responses. + * + * This service acts as a utility layer for Quartz operations, abstracting + * the API communication and providing both low-level and high-level fetch methods. + */ +export class QuartzActuatorService { + /** + * Fetch all job groups with their job names + * + * @param instance The Instance to fetch from + * @returns Promise with job groups and job names + */ + static async fetchJobGroups( + instance: Instance, + ): Promise> { + return instance.axios.get(uri`actuator/quartz/jobs`, { + headers: { Accept: 'application/json' }, + }); + } + + /** + * Fetch details of a specific job + * + * @param instance The Instance to fetch from + * @param groupName The job group name + * @param jobName The job name + * @returns Promise with job details including triggers + */ + static async fetchJobDetail( + instance: Instance, + groupName: string, + jobName: string, + ): Promise> { + return instance.axios.get( + uri`actuator/quartz/jobs/${groupName}/${jobName}`, + { headers: { Accept: 'application/json' } }, + ); + } + + /** + * Fetch all jobs in all groups with full details + * + * Fetches the list of job groups, then fetches each job individually, + * and aggregates them into a single array. + * Handles partial failures gracefully - returns successful jobs even if some fail. + * + * @param instance The Instance to fetch from + * @returns Promise with array of all job details + */ + static async fetchAllJobs(instance: Instance): Promise { + const response = await this.fetchJobGroups(instance); + const jobList = response.data; + const promises: Promise>[] = []; + + for (const group in jobList.groups) { + for (const jobName of jobList.groups[group].jobs) { + promises.push(this.fetchJobDetail(instance, group, jobName)); + } + } + + const results = await Promise.allSettled(promises); + return results + .filter((r) => r.status === 'fulfilled') + .map( + (r) => + (r as PromiseFulfilledResult>).value.data, + ); + } + + /** + * Fetch all trigger groups with their trigger names and paused status + * + * @param instance The Instance to fetch from + * @returns Promise with trigger groups, names, and paused status + */ + static async fetchTriggerGroups( + instance: Instance, + ): Promise> { + return instance.axios.get(uri`actuator/quartz/triggers`, { + headers: { Accept: 'application/json' }, + }); + } + + /** + * Fetch details of a specific trigger + * + * @param instance The Instance to fetch from + * @param groupName The trigger group name + * @param triggerName The trigger name + * @returns Promise with trigger details including type-specific configuration + */ + static async fetchTriggerDetail( + instance: Instance, + groupName: string, + triggerName: string, + ): Promise> { + return instance.axios.get( + uri`actuator/quartz/triggers/${groupName}/${triggerName}`, + { headers: { Accept: 'application/json' } }, + ); + } + + /** + * Fetch all triggers in all groups with full details + * + * Fetches the list of trigger groups, then fetches each trigger individually, + * and aggregates them into a single array. + * Handles partial failures gracefully - returns successful triggers even if some fail. + * + * @param instance The Instance to fetch from + * @returns Promise with array of all trigger details + */ + static async fetchAllTriggers(instance: Instance): Promise { + const response = await this.fetchTriggerGroups(instance); + const groupList = response.data; + const promises: Promise>[] = []; + + for (const group in groupList.groups) { + for (const triggerName of groupList.groups[group].triggers) { + promises.push(this.fetchTriggerDetail(instance, group, triggerName)); + } + } + + const results = await Promise.allSettled(promises); + return results + .filter((r) => r.status === 'fulfilled') + .map( + (r) => + (r as PromiseFulfilledResult>).value + .data, + ); + } + + /** + * Trigger (fire) a job immediately + * + * @param instance The Instance to operate on + * @param groupName The job group name + * @param jobName The job name + * @returns Promise with the operation result containing job details and trigger time + */ + static async triggerJob( + instance: Instance, + groupName: string, + jobName: string, + ): Promise> { + return instance.axios.post( + uri`actuator/quartz/jobs/${groupName}/${jobName}`, + { state: 'running' }, + ); + } +} + +/** + * Response structure from GET /actuator/quartz/jobs + */ +export interface JobGroupsResponse { + groups: { [groupName: string]: { jobs: string[] } }; +} + +/** + * Response structure from GET /actuator/quartz/jobs/{groupName}/{jobName} + */ +export interface JobDetail { + group: string; + name: string; + className: string; + description?: string; + durable: boolean; + requestRecovery: boolean; + data?: { [key: string]: string }; + triggers: Array<{ + group: string; + name: string; + previousFireTime?: string; + nextFireTime?: string; + priority: number; + }>; +} + +/** + * Response structure from GET /actuator/quartz/triggers + */ +export interface TriggerGroupsResponse { + groups: { + [groupName: string]: { + paused: boolean; + triggers: string[]; + }; + }; +} + +/** + * Response structure from POST /actuator/quartz/jobs/{groupName}/{jobName} + * Returned when triggering a job to run immediately + */ +export interface TriggerJobResponse { + group: string; + name: string; + className: string; + triggerTime: string; +} + +/** + * Response structure from GET /actuator/quartz/triggers/{groupName}/{triggerName} + * Includes type-specific trigger details (cron, simple, dailyTimeInterval, calendarInterval, custom) + */ +export interface TriggerDetail { + group: string; + name: string; + description?: string; + state: string; + type: 'cron' | 'simple' | 'dailyTimeInterval' | 'calendarInterval' | 'custom'; + priority: number; + startTime?: string; + endTime?: string; + previousFireTime?: string; + nextFireTime?: string; + finalFireTime?: string; + calendarName?: string; + data?: { [key: string]: string }; + + cron?: { + expression: string; + timeZone?: string; + }; + simple?: { + interval: number; + repeatCount: number; + timesTriggered: number; + }; + dailyTimeInterval?: { + interval: number; + daysOfWeek: number[]; + startTimeOfDay?: string; + endTimeOfDay?: string; + repeatCount: number; + timesTriggered: number; + }; + calendarInterval?: { + interval: number; + timeZone?: string; + timesTriggered: number; + preserveHourOfDayAcrossDaylightSavings: boolean; + skipDayIfHourDoesNotExist: boolean; + }; + custom?: { + trigger: string; + }; +} diff --git a/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts b/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts index 8f68282eeb7..dfa598da467 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/test-utils.ts @@ -75,6 +75,9 @@ export const render = (testComponent, options?): RenderResult => { stubs: { RouterLink: RouterLinkStub, 'sba-exchanges-chart': true, + 'font-awesome-icon': { + template: '', + }, }, }, }, diff --git a/spring-boot-admin-server-ui/src/main/frontend/theme.css b/spring-boot-admin-server-ui/src/main/frontend/theme.css index de81de43640..23f6e454003 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/theme.css +++ b/spring-boot-admin-server-ui/src/main/frontend/theme.css @@ -14,20 +14,20 @@ * limitations under the License. */ -@import "tailwindcss"; +@import 'tailwindcss'; @custom-variant dark (&:where(.dark, .dark *)); @plugin "@tailwindcss/forms"; @plugin "@tailwindcss/typography"; @theme { - --color-sba-50: var(--main-50); - --color-sba-100: var(--main-100); - --color-sba-200: var(--main-200); - --color-sba-300: var(--main-300); - --color-sba-400: var(--main-400); - --color-sba-500: var(--main-500); - --color-sba-600: var(--main-600); - --color-sba-700: var(--main-700); - --color-sba-800: var(--main-800); - --color-sba-900: var(--main-900); + --color-sba-50: var(--main-50); + --color-sba-100: var(--main-100); + --color-sba-200: var(--main-200); + --color-sba-300: var(--main-300); + --color-sba-400: var(--main-400); + --color-sba-500: var(--main-500); + --color-sba-600: var(--main-600); + --color-sba-700: var(--main-700); + --color-sba-800: var(--main-800); + --color-sba-900: var(--main-900); } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/__mocks__/quartz-data.ts b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/__mocks__/quartz-data.ts new file mode 100644 index 00000000000..762f0d879ae --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/__mocks__/quartz-data.ts @@ -0,0 +1,193 @@ +/* + * Copyright 2014-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Mock data for Quartz actuator API responses + * Used for testing UI components with realistic Quartz data + */ + +export const mockJobDetail = { + className: + 'de.codecentric.boot.admin.sample.QuartzJobsConfiguration$SampleJob', + data: { + 'job.key': 'job-value', + 'another.key': 'another-value', + }, + description: 'Sample job to demonstrate Quartz actuator endpoint', + durable: true, + group: 'samples', + name: 'sampleJob', + requestRecovery: false, + triggers: [ + { + group: 'samples', + name: 'sampleTrigger', + previousFireTime: '2026-03-26T14:11:11.333Z', + nextFireTime: '2026-03-26T14:11:21.333Z', + priority: 5, + }, + { + group: 'DEFAULT', + name: 'hourlyTrigger', + previousFireTime: '2026-03-26T13:31:31.342Z', + nextFireTime: '2026-03-26T14:31:31.342Z', + priority: 5, + }, + ], +}; + +export const mockJobDetailNoDescription = { + className: + 'de.codecentric.boot.admin.sample.QuartzJobsConfiguration$AnotherSampleJob', + data: {}, + durable: false, + group: 'samples', + name: 'anotherJob', + requestRecovery: true, + triggers: [], +}; + +export const mockJobGroupsResponse = { + groups: { + samples: { + jobs: ['sampleJob', 'anotherJob'], + }, + }, +}; + +export const mockSimpleTriggerDetail = { + group: 'samples', + name: 'sampleTrigger', + description: 'Trigger that executes sample job every 10 seconds', + state: 'NORMAL', + type: 'simple' as const, + startTime: '2026-03-26T13:31:31.333Z', + previousFireTime: '2026-03-26T14:11:21.333Z', + nextFireTime: '2026-03-26T14:11:31.333Z', + priority: 5, + data: { + 'trigger.key': 'trigger-value', + }, + simple: { + interval: 10000, + repeatCount: -1, + timesTriggered: 240, + }, +}; + +export const mockCronTriggerDetail = { + group: 'samples', + name: 'dailyTrigger', + description: 'Daily trigger at 3am', + state: 'NORMAL', + type: 'cron' as const, + previousFireTime: '2026-03-26T03:00:00.000Z', + nextFireTime: '2026-03-27T03:00:00.000Z', + priority: 5, + data: {}, + cron: { + expression: '0 0 3 * * ?', + timeZone: 'UTC', + }, +}; + +export const mockDailyTimeIntervalTriggerDetail = { + group: 'testing', + name: 'dailyTimeIntervalTrigger', + description: 'Runs every weekday during business hours', + state: 'PAUSED', + type: 'dailyTimeInterval' as const, + startTime: '2026-01-01T08:00:00.000Z', + endTime: '2026-12-31T17:00:00.000Z', + previousFireTime: '2026-03-26T16:00:00.000Z', + nextFireTime: '2026-03-27T08:00:00.000Z', + priority: 3, + data: {}, + dailyTimeInterval: { + interval: 60, // minutes + daysOfWeek: [2, 3, 4, 5, 6], // Mon-Fri + startTimeOfDay: '08:00:00', + endTimeOfDay: '17:00:00', + repeatCount: -1, + timesTriggered: 156, + }, +}; + +export const mockCalendarIntervalTriggerDetail = { + group: 'testing', + name: 'monthlyTrigger', + description: 'Runs monthly with DST handling', + state: 'NORMAL', + type: 'calendarInterval' as const, + previousFireTime: '2026-02-26T10:00:00.000Z', + nextFireTime: '2026-04-26T10:00:00.000Z', + priority: 5, + calendarName: 'businessDaysCalendar', + data: { + calendar: 'business-days', + }, + calendarInterval: { + interval: 1, // every 1 month + timeZone: 'Europe/Berlin', + timesTriggered: 3, + preserveHourOfDayAcrossDaylightSavings: true, + skipDayIfHourDoesNotExist: false, + }, +}; + +export const mockCustomTriggerDetail = { + group: 'testing', + name: 'customTrigger', + description: 'Custom trigger with specific implementation', + state: 'NORMAL', + type: 'custom' as const, + previousFireTime: '2026-03-26T12:00:00.000Z', + nextFireTime: '2026-03-26T14:00:00.000Z', + priority: 7, + data: {}, + custom: { + trigger: 'com.example.CustomQuartzTrigger', + }, +}; + +export const mockTriggerGroupsResponse = { + groups: { + samples: { + paused: false, + triggers: ['sampleTrigger', 'dailyTrigger'], + }, + testing: { + paused: true, + triggers: ['dailyTimeIntervalTrigger', 'monthlyTrigger', 'customTrigger'], + }, + }, +}; + +export const mockTriggerDetailNoDescription = { + group: 'samples', + name: 'hourlyTrigger', + state: 'NORMAL', + type: 'simple' as const, + priority: 5, + previousFireTime: '2026-03-26T13:31:31.342Z', + nextFireTime: '2026-03-26T14:31:31.342Z', + data: {}, + simple: { + interval: 3600000, + repeatCount: -1, + timesTriggered: 24, + }, +}; diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/error-alert.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/error-alert.vue new file mode 100644 index 00000000000..fd80730669b --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/error-alert.vue @@ -0,0 +1,23 @@ + + + diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.de.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.de.json index 6e87940f2a2..037207cef58 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.de.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.de.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Aktionen", + "active_triggers": "Aktive Auslöser", + "associated_triggers": "Zugeordnete Auslöser", + "associated_with_calendar": "Mit Kalender assoziiert", + "basic_information": "Grundinformationen", + "calendar": "Kalender", + "class": "Klasse", + "configuration": "Konfiguration", + "cron_expression": "Cron-Ausdruck", + "custom_trigger": "Benutzerdefinierter Auslöser", + "days_of_week": "Wochentage", + "description": "Beschreibung", + "details": "Details", + "disabled": "Deaktiviert", + "durable": "Persistent", + "enabled": "Aktiviert", + "end_time": "Endzeit", + "final_fire_time": "Endgültige Ausführungszeit", + "friday": "Freitag", + "group": "Gruppe", + "infinite": "Unbegrenzt", + "interval": "Intervall", + "job_data": "Jobdaten", + "job_name": "Jobname", + "jobs": "Jobs", + "key": "Schlüssel", "label": "Quartz", - "no_data": "Keine Quartz Informationen vorhanden" + "last_fire": "Letzte Ausführung", + "last_fire_time": "Letzte Ausführungszeit", + "monday": "Montag", + "name": "Name", + "next_fire": "Nächste Ausführung", + "next_fire_time": "Nächste Ausführungszeit", + "no": "Nein", + "no_data": "Keine Quartz-Informationen verfügbar", + "paused_triggers": "Pausierte Auslöser", + "preserve_hour_of_day": "Tagesstunde beibehalten", + "priority": "Priorität", + "recovery": "Wiederherstellung", + "repeat_count": "Anzahl Wiederholungen", + "request_recovery": "Wiederherstellung anfordern", + "saturday": "Samstag", + "schedule_configuration": "Zeitplankonfiguration", + "skip_day_if_hour_does_not_exist": "Tag überspringen, wenn Stunde nicht existiert", + "start_time": "Startzeit", + "state": "Status", + "sunday": "Sonntag", + "system_default": "Systemstandard", + "thursday": "Donnerstag", + "time_window": "Zeitfenster", + "time_zone": "Zeitzone", + "times_triggered": "Anzahl Auslösungen", + "timing_information": "Zeitinformationen", + "total": "insgesamt", + "trigger_data": "Auslöserdaten", + "trigger_failed": "Auslösung fehlgeschlagen", + "trigger_failed_message": "Fehler beim Auslösen des Jobs: {error}", + "trigger_failed_notification": "Fehler beim Auslösen des Jobs \"{jobName}\": {error}", + "trigger_fire_times": "Auslösezeiten", + "trigger_job_now": "Job jetzt auslösen", + "trigger_success_message": "Job \"{jobName}\" erfolgreich ausgelöst", + "trigger_success_title": "Job ausgelöst", + "triggers": "Auslöser", + "tuesday": "Dienstag", + "type": "Typ", + "value": "Wert", + "wednesday": "Mittwoch", + "yes": "Ja" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.en.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.en.json index 1f2233c3859..534d18122a4 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.en.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.en.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Actions", + "active_triggers": "Active Triggers", + "associated_triggers": "Associated Triggers", + "associated_with_calendar": "Associated with calendar", + "basic_information": "Basic Information", + "calendar": "Calendar", + "class": "Class", + "configuration": "Configuration", + "cron_expression": "Cron Expression", + "custom_trigger": "Custom Trigger", + "days_of_week": "Days of Week", + "description": "Description", + "details": "Details", + "disabled": "Disabled", + "durable": "Durable", + "enabled": "Enabled", + "end_time": "End Time", + "final_fire_time": "Final Fire Time", + "friday": "Friday", + "group": "Group", + "infinite": "Infinite", + "interval": "Interval", + "job_data": "Job Data", + "job_name": "Job Name", + "jobs": "Jobs", + "key": "Key", "label": "Quartz", - "no_data": "No Quartz information available" + "last_fire": "Last Fire", + "last_fire_time": "Last Fire Time", + "monday": "Monday", + "name": "Name", + "next_fire": "Next Fire", + "next_fire_time": "Next Fire Time", + "no": "No", + "no_data": "No Quartz information available", + "paused_triggers": "Paused Triggers", + "preserve_hour_of_day": "Preserve Hour of Day", + "priority": "Priority", + "recovery": "Recovery", + "repeat_count": "Repeat Count", + "request_recovery": "Request Recovery", + "saturday": "Saturday", + "schedule_configuration": "Schedule Configuration", + "skip_day_if_hour_does_not_exist": "Skip Day If Hour Does Not Exist", + "start_time": "Start Time", + "state": "State", + "sunday": "Sunday", + "system_default": "System Default", + "thursday": "Thursday", + "time_window": "Time Window", + "time_zone": "Time Zone", + "times_triggered": "Times Triggered", + "timing_information": "Timing Information", + "total": "total", + "trigger_data": "Trigger Data", + "trigger_failed": "Trigger Failed", + "trigger_failed_message": "Failed to trigger job: {error}", + "trigger_failed_notification": "Failed to trigger job \"{jobName}\": {error}", + "trigger_fire_times": "Trigger Fire Times", + "trigger_job_now": "Trigger Job Now", + "trigger_success_message": "Job \"{jobName}\" triggered successfully", + "trigger_success_title": "Job Triggered", + "triggers": "Triggers", + "tuesday": "Tuesday", + "type": "Type", + "value": "Value", + "wednesday": "Wednesday", + "yes": "Yes" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.es.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.es.json index 3e1b325b760..9c963ee52ca 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.es.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.es.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Acciones", + "active_triggers": "Disparadores Activos", + "associated_triggers": "Disparadores Asociados", + "associated_with_calendar": "Asociado con calendario", + "basic_information": "Información Básica", + "calendar": "Calendario", + "class": "Clase", + "configuration": "Configuración", + "cron_expression": "Expresión Cron", + "custom_trigger": "Disparador Personalizado", + "days_of_week": "Días de la Semana", + "description": "Descripción", + "details": "Detalles", + "disabled": "Deshabilitado", + "durable": "Duradero", + "enabled": "Habilitado", + "end_time": "Hora de Fin", + "final_fire_time": "Tiempo Final de Disparo", + "friday": "Viernes", + "group": "Grupo", + "infinite": "Infinito", + "interval": "Intervalo", + "job_data": "Datos del Trabajo", + "job_name": "Nombre del Trabajo", + "jobs": "Trabajos", + "key": "Clave", "label": "Quartz", - "no_data": "No hay información de Quartz disponible" + "last_fire": "Último Disparo", + "last_fire_time": "Último Tiempo de Disparo", + "monday": "Lunes", + "name": "Nombre", + "next_fire": "Próximo Disparo", + "next_fire_time": "Próximo Tiempo de Disparo", + "no": "No", + "no_data": "No hay información de Quartz disponible", + "paused_triggers": "Disparadores Pausados", + "preserve_hour_of_day": "Preservar Hora del Día", + "priority": "Prioridad", + "recovery": "Recuperación", + "repeat_count": "Número de Repeticiones", + "request_recovery": "Solicitar Recuperación", + "saturday": "Sábado", + "schedule_configuration": "Configuración del Horario", + "skip_day_if_hour_does_not_exist": "Omitir Día si la Hora No Existe", + "start_time": "Hora de Inicio", + "state": "Estado", + "sunday": "Domingo", + "system_default": "Predeterminado del Sistema", + "thursday": "Jueves", + "time_window": "Ventana de Tiempo", + "time_zone": "Zona Horaria", + "times_triggered": "Veces Disparadas", + "timing_information": "Información de Tiempo", + "total": "total", + "trigger_data": "Datos del Disparador", + "trigger_failed": "Error al activar", + "trigger_failed_message": "Error al activar el trabajo: {error}", + "trigger_failed_notification": "Error al activar el trabajo \"{jobName}\": {error}", + "trigger_fire_times": "Tiempos de Disparo", + "trigger_job_now": "Ejecutar Trabajo Ahora", + "trigger_success_message": "Trabajo \"{jobName}\" activado exitosamente", + "trigger_success_title": "Trabajo Activado", + "triggers": "Disparadores", + "tuesday": "Martes", + "type": "Tipo", + "value": "Valor", + "wednesday": "Miércoles", + "yes": "Sí" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.fr.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.fr.json index 12f23d297e4..97f3abb0d58 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.fr.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.fr.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Actions", + "active_triggers": "Déclencheurs Actifs", + "associated_triggers": "Déclencheurs Associés", + "associated_with_calendar": "Associé au calendrier", + "basic_information": "Informations de Base", + "calendar": "Calendrier", + "class": "Classe", + "configuration": "Configuration", + "cron_expression": "Expression Cron", + "custom_trigger": "Déclencheur Personnalisé", + "days_of_week": "Jours de la Semaine", + "description": "Description", + "details": "Détails", + "disabled": "Désactivé", + "durable": "Durable", + "enabled": "Activé", + "end_time": "Heure de Fin", + "final_fire_time": "Heure du Dernier Déclenchement", + "friday": "Vendredi", + "group": "Groupe", + "infinite": "Infini", + "interval": "Intervalle", + "job_data": "Données de la Tâche", + "job_name": "Nom de la Tâche", + "jobs": "Tâches", + "key": "Clé", "label": "Quartz", - "no_data": "Aucune information disponible sur Quartz" + "last_fire": "Dernier Déclenchement", + "last_fire_time": "Heure du Dernier Déclenchement", + "monday": "Lundi", + "name": "Nom", + "next_fire": "Prochain Déclenchement", + "next_fire_time": "Heure du Prochain Déclenchement", + "no": "Non", + "no_data": "Aucune information disponible sur Quartz", + "paused_triggers": "Déclencheurs en Pause", + "preserve_hour_of_day": "Préserver l'Heure du Jour", + "priority": "Priorité", + "recovery": "Récupération", + "repeat_count": "Nombre de Répétitions", + "request_recovery": "Demander la Récupération", + "saturday": "Samedi", + "schedule_configuration": "Configuration de l'Horaire", + "skip_day_if_hour_does_not_exist": "Ignorer le Jour si l'Heure n'Existe Pas", + "start_time": "Heure de Début", + "state": "État", + "sunday": "Dimanche", + "system_default": "Par Défaut du Système", + "thursday": "Jeudi", + "time_window": "Fenêtre de Temps", + "time_zone": "Fuseau Horaire", + "times_triggered": "Nombre de Déclenchements", + "timing_information": "Informations de Synchronisation", + "total": "total", + "trigger_data": "Données du Déclencheur", + "trigger_failed": "Erreur d'activation", + "trigger_failed_message": "Erreur lors du déclenchement du travail: {error}", + "trigger_failed_notification": "Erreur lors du déclenchement du travail \"{jobName}\": {error}", + "trigger_fire_times": "Heures de Déclenchement", + "trigger_job_now": "Déclencher la Tâche Maintenant", + "trigger_success_message": "Travail \"{jobName}\" déclenché avec succès", + "trigger_success_title": "Travail Déclenché", + "triggers": "Déclencheurs", + "tuesday": "Mardi", + "type": "Type", + "value": "Valeur", + "wednesday": "Mercredi", + "yes": "Oui" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.is.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.is.json index f81292cd732..7dff69ba45d 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.is.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.is.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Aðgerðir", + "active_triggers": "Virkir Kveikir", + "associated_triggers": "Tengdir Kveikir", + "associated_with_calendar": "Tengt dagatalinu", + "basic_information": "Grunnupplýsingar", + "calendar": "Dagatal", + "class": "Flokkur", + "configuration": "Stilling", + "cron_expression": "Cron-Orðatiltæki", + "custom_trigger": "Sérsniðinn Kveikir", + "days_of_week": "Dagar Vikunnar", + "description": "Lýsing", + "details": "Smáatriði", + "disabled": "Óvirkt", + "durable": "Stöðugur", + "enabled": "Virkt", + "end_time": "Lokatími", + "final_fire_time": "Lokatími Kveikju", + "friday": "Föstudagur", + "group": "Hópur", + "infinite": "Óendanlegur", + "interval": "Millibil", + "job_data": "Gögn Verks", + "job_name": "Heiti Verks", + "jobs": "Verk", + "key": "Lykill", "label": "Quartz", - "no_data": "Engar upplýsingar fást um Quartz" + "last_fire": "Síðasta Kveikiun", + "last_fire_time": "Tími Síðustu Kveikju", + "monday": "Mánudagur", + "name": "Heiti", + "next_fire": "Næsta Kveikiun", + "next_fire_time": "Næsta Kveikiun", + "no": "Nei", + "no_data": "Engar upplýsingar um Quartz", + "paused_triggers": "Stöðvuðir Kveikir", + "preserve_hour_of_day": "Varðveita Tíma Dags", + "priority": "Forgangur", + "recovery": "Endurheimtur", + "repeat_count": "Endurtaka Fjöldi", + "request_recovery": "Óska eftir Endurheimtun", + "saturday": "Laugardagur", + "schedule_configuration": "Dagskrársetting", + "skip_day_if_hour_does_not_exist": "Sleppa Degi ef Tími Ekki Til", + "start_time": "Upphafstími", + "state": "Staða", + "sunday": "Sunnudagur", + "system_default": "Sjálfgefið Kerfi", + "thursday": "Fimmtudagur", + "time_window": "Tímagluggi", + "time_zone": "Tímabelti", + "times_triggered": "Fjöldi Kveikinga", + "timing_information": "Tíðarupplýsingar", + "total": "samtals", + "trigger_data": "Gögn Kveikis", + "trigger_failed": "Hnötunargildi mistókst", + "trigger_failed_message": "Villa við að framkalla starf: {error}", + "trigger_failed_notification": "Villa við að framkalla starf \"{jobName}\": {error}", + "trigger_fire_times": "Tímar Kveikinga", + "trigger_job_now": "Kveikja Verk Núna", + "trigger_success_message": "Starf \"{jobName}\" framkallað með góðum árangri", + "trigger_success_title": "Starf Framkallað", + "triggers": "Kveikir", + "tuesday": "Þriðjudagur", + "type": "Tegund", + "value": "Gildi", + "wednesday": "Miðvikudagur", + "yes": "Já" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ko.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ko.json index da1b2229fef..c9873a8ab11 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ko.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ko.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "작업", + "active_triggers": "활성 트리거", + "associated_triggers": "연결된 트리거", + "associated_with_calendar": "달력과 연결됨", + "basic_information": "기본 정보", + "calendar": "달력", + "class": "클래스", + "configuration": "구성", + "cron_expression": "Cron 표현식", + "custom_trigger": "사용자 정의 트리거", + "days_of_week": "요일", + "description": "설명", + "details": "세부 정보", + "disabled": "비활성화됨", + "durable": "지속성", + "enabled": "활성화됨", + "end_time": "종료 시간", + "final_fire_time": "최종 실행 시간", + "friday": "금요일", + "group": "그룹", + "infinite": "무한", + "interval": "간격", + "job_data": "작업 데이터", + "job_name": "작업 이름", + "jobs": "작업", + "key": "키", "label": "Quartz", - "no_data": "사용 가능한 Quartz 정보가 없습니다." + "last_fire": "마지막 실행", + "last_fire_time": "마지막 실행 시간", + "monday": "월요일", + "name": "이름", + "next_fire": "다음 실행", + "next_fire_time": "다음 실행 시간", + "no": "아니오", + "no_data": "사용 가능한 Quartz 정보가 없습니다.", + "paused_triggers": "일시 중지된 트리거", + "preserve_hour_of_day": "시간 유지", + "priority": "우선순위", + "recovery": "복구", + "repeat_count": "반복 횟수", + "request_recovery": "복구 요청", + "saturday": "토요일", + "schedule_configuration": "일정 구성", + "skip_day_if_hour_does_not_exist": "시간이 없으면 날짜 건너뛰기", + "start_time": "시작 시간", + "state": "상태", + "sunday": "일요일", + "system_default": "시스템 기본값", + "thursday": "목요일", + "time_window": "시간 범위", + "time_zone": "시간대", + "times_triggered": "실행된 횟수", + "timing_information": "타이밍 정보", + "total": "총계", + "trigger_data": "트리거 데이터", + "trigger_failed": "트리거 실패", + "trigger_failed_message": "작업 트리거 실패: {error}", + "trigger_failed_notification": "작업 \"{jobName}\" 트리거 실패: {error}", + "trigger_fire_times": "트리거 실행 시간", + "trigger_job_now": "지금 작업 실행", + "trigger_success_message": "작업 \"{jobName}\"이(가) 성공적으로 트리거되었습니다", + "trigger_success_title": "작업 트리거됨", + "triggers": "트리거", + "tuesday": "화요일", + "type": "유형", + "value": "값", + "wednesday": "수요일", + "yes": "예" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.pt-BR.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.pt-BR.json index 12b570395be..168295c5b1f 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.pt-BR.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.pt-BR.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Ações", + "active_triggers": "Disparadores Ativos", + "associated_triggers": "Disparadores Associados", + "associated_with_calendar": "Associado ao calendário", + "basic_information": "Informações Básicas", + "calendar": "Calendário", + "class": "Classe", + "configuration": "Configuração", + "cron_expression": "Expressão Cron", + "custom_trigger": "Disparador Personalizado", + "days_of_week": "Dias da Semana", + "description": "Descrição", + "details": "Detalhes", + "disabled": "Desabilitado", + "durable": "Durável", + "enabled": "Habilitado", + "end_time": "Hora de Término", + "final_fire_time": "Tempo do Último Disparo Final", + "friday": "Sexta-feira", + "group": "Grupo", + "infinite": "Infinito", + "interval": "Intervalo", + "job_data": "Dados da Tarefa", + "job_name": "Nome da Tarefa", + "jobs": "Tarefas", + "key": "Chave", "label": "Quartz", - "no_data": "Nenhuma informação de Quartz disponível" + "last_fire": "Último Disparo", + "last_fire_time": "Tempo do Último Disparo", + "monday": "Segunda-feira", + "name": "Nome", + "next_fire": "Próximo Disparo", + "next_fire_time": "Próximo Tempo de Disparo", + "no": "Não", + "no_data": "Nenhuma informação de Quartz disponível", + "paused_triggers": "Disparadores em Pausa", + "preserve_hour_of_day": "Preservar Hora do Dia", + "priority": "Prioridade", + "recovery": "Recuperação", + "repeat_count": "Número de Repetições", + "request_recovery": "Solicitar Recuperação", + "saturday": "Sábado", + "schedule_configuration": "Configuração do Agendamento", + "skip_day_if_hour_does_not_exist": "Pular Dia se a Hora Não Existir", + "start_time": "Hora de Início", + "state": "Estado", + "sunday": "Domingo", + "system_default": "Padrão do Sistema", + "thursday": "Quinta-feira", + "time_window": "Janela de Tempo", + "time_zone": "Fuso Horário", + "times_triggered": "Vezes Disparadas", + "timing_information": "Informações de Tempo", + "total": "total", + "trigger_data": "Dados do Disparador", + "trigger_failed": "Falha ao acionador", + "trigger_failed_message": "Falha ao acionar o trabalho: {error}", + "trigger_failed_notification": "Falha ao acionar o trabalho \"{jobName}\": {error}", + "trigger_fire_times": "Tempos de Disparo", + "trigger_job_now": "Executar Tarefa Agora", + "trigger_success_message": "Trabalho \"{jobName}\" acionado com sucesso", + "trigger_success_title": "Trabalho Acionado", + "triggers": "Disparadores", + "tuesday": "Terça-feira", + "type": "Tipo", + "value": "Valor", + "wednesday": "Quarta-feira", + "yes": "Sim" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ru.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ru.json index 46950bbae4b..71259515772 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ru.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.ru.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "Действия", + "active_triggers": "Активные Триггеры", + "associated_triggers": "Связанные Триггеры", + "associated_with_calendar": "Связано с календарем", + "basic_information": "Основная Информация", + "calendar": "Календарь", + "class": "Класс", + "configuration": "Конфигурация", + "cron_expression": "Выражение Cron", + "custom_trigger": "Пользовательский Триггер", + "days_of_week": "Дни Недели", + "description": "Описание", + "details": "Детали", + "disabled": "Отключено", + "durable": "Постоянный", + "enabled": "Включено", + "end_time": "Время Окончания", + "final_fire_time": "Время Последнего Срабатывания", + "friday": "Пятница", + "group": "Группа", + "infinite": "Бесконечный", + "interval": "Интервал", + "job_data": "Данные Задания", + "job_name": "Имя Задания", + "jobs": "Задания", + "key": "Ключ", "label": "Quartz", - "no_data": "Нет информации о Quartz" + "last_fire": "Последнее Срабатывание", + "last_fire_time": "Время Последнего Срабатывания", + "monday": "Понедельник", + "name": "Имя", + "next_fire": "Следующее Срабатывание", + "next_fire_time": "Время Следующего Срабатывания", + "no": "Нет", + "no_data": "Информация о Quartz недоступна", + "paused_triggers": "Приостановленные Триггеры", + "preserve_hour_of_day": "Сохранить Час Дня", + "priority": "Приоритет", + "recovery": "Восстановление", + "repeat_count": "Количество Повторений", + "request_recovery": "Запрос Восстановления", + "saturday": "Суббота", + "schedule_configuration": "Конфигурация Расписания", + "skip_day_if_hour_does_not_exist": "Пропустить День, если Час Не Существует", + "start_time": "Время Начала", + "state": "Состояние", + "sunday": "Воскресенье", + "system_default": "По Умолчанию Системы", + "thursday": "Четверг", + "time_window": "Временное Окно", + "time_zone": "Часовой Пояс", + "times_triggered": "Количество Срабатываний", + "timing_information": "Информация о Времени", + "total": "всего", + "trigger_data": "Данные Триггера", + "trigger_failed": "Ошибка запуска", + "trigger_failed_message": "Ошибка при запуске задания: {error}", + "trigger_failed_notification": "Ошибка при запуске задания \"{jobName}\": {error}", + "trigger_fire_times": "Время Срабатывания", + "trigger_job_now": "Выполнить Задание Сейчас", + "trigger_success_message": "Задание \"{jobName}\" успешно запущено", + "trigger_success_title": "Задание Запущено", + "triggers": "Триггеры", + "tuesday": "Вторник", + "type": "Тип", + "value": "Значение", + "wednesday": "Среда", + "yes": "Да" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-CN.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-CN.json index 5adfe3da385..599a3e27a47 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-CN.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-CN.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "操作", + "active_triggers": "活跃触发器", + "associated_triggers": "关联的触发器", + "associated_with_calendar": "与日历关联", + "basic_information": "基本信息", + "calendar": "日历", + "class": "类", + "configuration": "配置", + "cron_expression": "Cron 表达式", + "custom_trigger": "自定义触发器", + "days_of_week": "星期几", + "description": "描述", + "details": "详情", + "disabled": "禁用", + "durable": "持久的", + "enabled": "启用", + "end_time": "结束时间", + "final_fire_time": "最终执行时间", + "friday": "星期五", + "group": "组", + "infinite": "无限", + "interval": "间隔", + "job_data": "作业数据", + "job_name": "作业名称", + "jobs": "作业", + "key": "键", "label": "Quartz", - "no_data": "没有可用的石英信息" + "last_fire": "最后一次触发", + "last_fire_time": "最后一次执行时间", + "monday": "星期一", + "name": "名称", + "next_fire": "下一次触发", + "next_fire_time": "下一次执行时间", + "no": "否", + "no_data": "没有可用的 Quartz 信息", + "paused_triggers": "暂停的触发器", + "preserve_hour_of_day": "保留当天小时", + "priority": "优先级", + "recovery": "恢复", + "repeat_count": "重复次数", + "request_recovery": "请求恢复", + "saturday": "星期六", + "schedule_configuration": "日程配置", + "skip_day_if_hour_does_not_exist": "如果小时不存在,跳过该天", + "start_time": "开始时间", + "state": "状态", + "sunday": "星期日", + "system_default": "系统默认", + "thursday": "星期四", + "time_window": "时间窗口", + "time_zone": "时区", + "times_triggered": "执行次数", + "timing_information": "时间信息", + "total": "总计", + "trigger_data": "触发器数据", + "trigger_failed": "触发失败", + "trigger_failed_message": "触发任务失败: {error}", + "trigger_failed_notification": "触发任务 \"{jobName}\" 失败: {error}", + "trigger_fire_times": "触发时间", + "trigger_job_now": "立即执行作业", + "trigger_success_message": "任务 \"{jobName}\" 已成功触发", + "trigger_success_title": "任务已触发", + "triggers": "触发器", + "tuesday": "星期二", + "type": "类型", + "value": "值", + "wednesday": "星期三", + "yes": "是" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-TW.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-TW.json index d5a109abd44..e625e0e18f8 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-TW.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/i18n.zh-TW.json @@ -1,8 +1,74 @@ { "instances": { "quartz": { + "actions": "操作", + "active_triggers": "活躍觸發器", + "associated_triggers": "相關聯的觸發器", + "associated_with_calendar": "與日曆關聯", + "basic_information": "基本信息", + "calendar": "日曆", + "class": "類別", + "configuration": "配置", + "cron_expression": "Cron 表達式", + "custom_trigger": "自訂觸發器", + "days_of_week": "星期幾", + "description": "描述", + "details": "詳細信息", + "disabled": "停用", + "durable": "持久", + "enabled": "啟用", + "end_time": "結束時間", + "final_fire_time": "最終執行時間", + "friday": "星期五", + "group": "組", + "infinite": "無限", + "interval": "間隔", + "job_data": "工作數據", + "job_name": "工作名稱", + "jobs": "工作", + "key": "鍵", "label": "Quartz", - "no_data": "沒有可用的 Quartz 資訊" + "last_fire": "最後一次觸發", + "last_fire_time": "最後一次執行時間", + "monday": "星期一", + "name": "名稱", + "next_fire": "下次觸發", + "next_fire_time": "下次執行時間", + "no": "否", + "no_data": "沒有可用的 Quartz 資訊", + "paused_triggers": "暫停的觸發器", + "preserve_hour_of_day": "保留當天時刻", + "priority": "優先權", + "recovery": "恢復", + "repeat_count": "重複次數", + "request_recovery": "請求恢復", + "saturday": "星期六", + "schedule_configuration": "日程配置", + "skip_day_if_hour_does_not_exist": "如果時刻不存在,跳過該天", + "start_time": "開始時間", + "state": "狀態", + "sunday": "星期日", + "system_default": "系統預設", + "thursday": "星期四", + "time_window": "時間窗口", + "time_zone": "時區", + "times_triggered": "執行次數", + "timing_information": "時間信息", + "total": "總計", + "trigger_data": "觸發器數據", + "trigger_failed": "觸發失敗", + "trigger_failed_message": "觸發任務失敗: {error}", + "trigger_failed_notification": "觸發任務 \"{jobName}\" 失敗: {error}", + "trigger_fire_times": "觸發時間", + "trigger_job_now": "立即執行工作", + "trigger_success_message": "任務 \"{jobName}\" 已成功觸發", + "trigger_success_title": "任務已觸發", + "triggers": "觸發器", + "tuesday": "星期二", + "type": "類型", + "value": "值", + "wednesday": "星期三", + "yes": "是" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/index.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/index.vue index 5080f6014cc..b3fabdf2f91 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/index.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/quartz/index.vue @@ -1,173 +1,142 @@