feat(status-page): add configurable time range for component history (#4222)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
This commit is contained in:
wy471x
2026-07-23 00:23:37 +08:00
committed by GitHub
co-authored by Claude Opus 4.7 Duansg Tomsun28
parent 032a4852f7
commit 4b217e7097
15 changed files with 170 additions and 63 deletions
@@ -62,15 +62,20 @@ public class StatusPagePublicController {
@GetMapping("/component")
@Operation(summary = "Query Status Page Components")
public ResponseEntity<Message<List<ComponentStatus>>> queryStatusPageComponent() {
List<ComponentStatus> componentStatusList = statusPageService.queryComponentsStatus();
public ResponseEntity<Message<List<ComponentStatus>>> queryStatusPageComponent(
@Parameter(description = "Number of days of history", example = "30")
@RequestParam(defaultValue = "30") int days) {
List<ComponentStatus> componentStatusList = statusPageService.queryComponentsStatus(days);
return ResponseEntity.ok(Message.success(componentStatusList));
}
@GetMapping("/component/{id}")
@Operation(summary = "Query Status Page Component")
public ResponseEntity<Message<ComponentStatus>> queryStatusPageComponent(@PathVariable("id") final long id) {
ComponentStatus componentStatus = statusPageService.queryComponentStatus(id);
public ResponseEntity<Message<ComponentStatus>> queryStatusPageComponent(
@PathVariable("id") final long id,
@Parameter(description = "Number of days of history", example = "30")
@RequestParam(defaultValue = "30") int days) {
ComponentStatus componentStatus = statusPageService.queryComponentStatus(id, days);
return ResponseEntity.ok(Message.success(componentStatus));
}
@@ -83,17 +83,19 @@ public interface StatusPageService {
/**
* query status page components status.
*
* @param days number of days of history to return
* @return status page components status
*/
List<ComponentStatus> queryComponentsStatus();
List<ComponentStatus> queryComponentsStatus(int days);
/**
* query status page component status.
*
* @param id status page component id
* @param id status page component id
* @param days number of days of history to return
* @return status page component status
*/
ComponentStatus queryComponentStatus(long id);
ComponentStatus queryComponentStatus(long id, int days);
/**
* query status page incidents.
@@ -58,8 +58,6 @@ import org.springframework.util.StringUtils;
@RequiredArgsConstructor
public class StatusPageServiceImpl implements StatusPageService {
private static final int HISTORY_SPAN_DAYS = 29;
@Autowired
private StatusPageOrgDao statusPageOrgDao;
@@ -126,7 +124,12 @@ public class StatusPageServiceImpl implements StatusPageService {
}
@Override
public List<ComponentStatus> queryComponentsStatus() {
public List<ComponentStatus> queryComponentsStatus(int days) {
if (days < 1 || days > 90) {
throw new IllegalArgumentException("days must be between 1 and 90");
}
int historySpanDays = days - 1;
List<StatusPageComponent> components = statusPageComponentDao.findAll();
List<ComponentStatus> componentStatusList = new LinkedList<>();
for (StatusPageComponent component : components) {
@@ -150,11 +153,11 @@ public class StatusPageServiceImpl implements StatusPageService {
.findStatusPageHistoriesByComponentIdAndTimestampBetween(component.getId(), todayStartTimestamp, nowTimestamp);
StatusPageHistory todayStatus = combineOneDayStatusPageHistory(todayStatusPageHistoryList, component, nowTimestamp);
histories.add(todayStatus);
// query 30d component status history
// query component status history for the configured number of days
long preTimestamp = now
.atZone(zoneId)
.toLocalDate()
.minusDays(HISTORY_SPAN_DAYS)
.minusDays(historySpanDays)
.atStartOfDay(zoneId)
.toInstant()
.toEpochMilli();
@@ -167,7 +170,7 @@ public class StatusPageServiceImpl implements StatusPageService {
.atZone(zoneId)
.minusSeconds(1); // yesterday 23:59:59 local time
for (int i = 0; i < HISTORY_SPAN_DAYS; i++) {
for (int i = 0; i < historySpanDays; i++) {
long endTimestamp = end.toInstant().toEpochMilli();
long startTimestamp = end.toLocalDate()
@@ -242,7 +245,12 @@ public class StatusPageServiceImpl implements StatusPageService {
}
@Override
public ComponentStatus queryComponentStatus(long id) {
public ComponentStatus queryComponentStatus(long id, int days) {
if (days < 1 || days > 90) {
throw new IllegalArgumentException("days must be between 1 and 90");
}
int historySpanDays = days - 1;
StatusPageComponent component = statusPageComponentDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException("component not found"));
@@ -272,11 +280,11 @@ public class StatusPageServiceImpl implements StatusPageService {
histories.add(todayStatus);
// Previous HISTORY_SPAN_DAYS days (excluding today)
// Previous historySpanDays days (excluding today)
long preTimestamp = now
.atZone(zoneId)
.toLocalDate()
.minusDays(HISTORY_SPAN_DAYS)
.minusDays(historySpanDays)
.atStartOfDay(zoneId)
.toInstant()
.toEpochMilli();
@@ -292,7 +300,7 @@ public class StatusPageServiceImpl implements StatusPageService {
.atZone(zoneId)
.minusSeconds(1); // yesterday 23:59:59 local time
for (int i = 0; i < HISTORY_SPAN_DAYS; i++) {
for (int i = 0; i < historySpanDays; i++) {
long endTimestamp = end.toInstant().toEpochMilli();
long startTimestamp = end.toLocalDate()
@@ -88,7 +88,7 @@ class StatusPagePublicControllerTest {
public void testQueryStatusPageComponent() throws Exception {
List<ComponentStatus> componentStatusList = Collections.singletonList(new ComponentStatus());
when(statusPageService.queryComponentsStatus()).thenReturn(componentStatusList);
when(statusPageService.queryComponentsStatus(30)).thenReturn(componentStatusList);
mockMvc.perform(get("/api/status/page/public/component")
.accept(MediaType.APPLICATION_JSON))
@@ -96,11 +96,24 @@ class StatusPagePublicControllerTest {
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
@Test
public void testQueryStatusPageComponentWithDays() throws Exception {
List<ComponentStatus> componentStatusList = Collections.singletonList(new ComponentStatus());
when(statusPageService.queryComponentsStatus(7)).thenReturn(componentStatusList);
mockMvc.perform(get("/api/status/page/public/component")
.param("days", "7")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
@Test
public void testQueryStatusPageComponentById() throws Exception {
ComponentStatus componentStatus = new ComponentStatus();
when(statusPageService.queryComponentStatus(1L)).thenReturn(componentStatus);
when(statusPageService.queryComponentStatus(1L, 30)).thenReturn(componentStatus);
mockMvc.perform(get("/api/status/page/public/component/1")
.accept(MediaType.APPLICATION_JSON))
@@ -108,6 +121,19 @@ class StatusPagePublicControllerTest {
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
@Test
public void testQueryStatusPageComponentByIdWithDays() throws Exception {
ComponentStatus componentStatus = new ComponentStatus();
when(statusPageService.queryComponentStatus(1L, 7)).thenReturn(componentStatus);
mockMvc.perform(get("/api/status/page/public/component/1")
.param("days", "7")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
@Test
public void testQueryStatusPageIncident() throws Exception {
@@ -99,8 +99,9 @@ class StatusPageServiceImplTimeTest {
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
.thenReturn(List.of(before, after));
List<ComponentStatus> result = service.queryComponentsStatus();
assertEquals(30, result.get(0).getHistory().size());
assertEquals(30, service.queryComponentsStatus(30).get(0).getHistory().size());
assertEquals(1, service.queryComponentsStatus(1).get(0).getHistory().size());
assertEquals(90, service.queryComponentsStatus(90).get(0).getHistory().size());
}
@Test
@@ -114,7 +115,7 @@ class StatusPageServiceImplTimeTest {
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
.thenReturn(List.of(history));
List<ComponentStatus> result = service.queryComponentsStatus();
List<ComponentStatus> result = service.queryComponentsStatus(30);
assertEquals(30, result.get(0).getHistory().size());
}
@@ -128,7 +129,7 @@ class StatusPageServiceImplTimeTest {
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
.thenReturn(List.of(history));
List<ComponentStatus> result = service.queryComponentsStatus();
List<ComponentStatus> result = service.queryComponentsStatus(30);
assertEquals(30, result.get(0).getHistory().size());
}
@@ -92,6 +92,11 @@
</div>
</div>
<div style="margin-top: 20px">
<div style="width: 60%; margin: 0 auto 10px; display: flex; align-items: center; justify-content: flex-end">
<nz-select style="width: 120px" [(ngModel)]="historyDays" (ngModelChange)="onHistoryDaysChange()">
<nz-option *ngFor="let d of historyDaysOptions" [nzValue]="d" [nzLabel]="getHistoryDaysLabel(d)"></nz-option>
</nz-select>
</div>
<div *ngFor="let component of componentStatus" class="component-status br-8">
<div style="margin: 10px; display: flex; justify-content: space-between">
<div style="font-weight: bold; font-size: 1rem">
@@ -119,7 +124,7 @@
<span nz-icon style="margin-left: 5px" nzType="stop" nzTheme="outline"></span>
</div>
</div>
<div style="margin-left: 10px; margin-top: 10px; margin-bottom: 10px; display: flex">
<div style="margin-left: 10px; margin-top: 10px; margin-bottom: 10px; display: flex; gap: 2px">
<div
*ngFor="let historyItem of component.history"
class="history-block br-8"
@@ -141,7 +146,7 @@
</div>
<div style="margin: 10px; display: flex; justify-content: space-between; font-size: 0.75rem">
<span>{{ 'status.public.today' | i18n }}</span>
<span>{{ 'status.public.30-day' | i18n }}</span>
<span>{{ getHistoryDaysAgoLabel() }}</span>
</div>
</div>
</div>
@@ -64,9 +64,8 @@
}
.history-block {
width: 2.33%;
flex: 1;
height: 60px;
margin-right: 1%
}
.status-incident {
@@ -21,7 +21,6 @@ import { Component, Inject, OnInit } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, TitleService } from '@delon/theme';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { switchMap } from 'rxjs';
import { Message } from '../../pojo/Message';
import { StatusPageComponentStatus } from '../../pojo/StatusPageComponentStatus';
@@ -52,6 +51,9 @@ export class StatusPublicComponent implements OnInit {
// component or incident
showMode: string = 'component';
historyDays: number = 30;
historyDaysOptions: number[] = [1, 7, 30, 90];
pageIndex: number = 1;
pageSize: number = 9999;
incidentYear: Date = new Date();
@@ -63,40 +65,65 @@ export class StatusPublicComponent implements OnInit {
this.loadStatusPageOrg();
}
onHistoryDaysChange() {
this.loadComponentStatus();
}
getHistoryDaysLabel(days: number): string {
if (days === 1) {
return this.i18nSvc.fanyi('status.public.24-hour');
}
return `${days} ${this.i18nSvc.fanyi('status.public.days')}`;
}
getHistoryDaysAgoLabel(): string {
if (this.historyDays === 1) {
return this.i18nSvc.fanyi('status.public.24-hour-ago');
}
return `${this.historyDays} ${this.i18nSvc.fanyi('status.public.days-ago')}`;
}
loadComponentStatus() {
this.loading = true;
let componentLoad$ = this.statusPagePublicService.getStatusPageComponents(this.historyDays).subscribe(
(message: Message<StatusPageComponentStatus[]>) => {
if (message.code !== 0) {
this.notifySvc.error(message.msg, '');
} else {
this.componentStatus = message.data;
}
this.loading = false;
componentLoad$.unsubscribe();
},
error => {
this.loading = false;
this.notifySvc.error(error.msg, '');
componentLoad$.unsubscribe();
}
);
}
loadStatusPageOrg() {
this.loading = true;
let loadInit$ = this.statusPagePublicService
.getStatusPageOrg()
.pipe(
switchMap((message: Message<StatusPageOrg>) => {
if (message.code === 0) {
this.statusOrg = message.data;
this.titleService.setTitle(`${this.statusOrg.name} ${this.i18nSvc.fanyi('menu.advanced.status')}`);
} else {
this.statusOrg = new StatusPageOrg();
console.log(message.msg);
this.notifySvc.error(message.msg, '');
throw new Error(message.msg);
}
return this.statusPagePublicService.getStatusPageComponents();
})
)
.subscribe(
(message: Message<StatusPageComponentStatus[]>) => {
if (message.code !== 0) {
this.notifySvc.error(message.msg, '');
} else {
this.componentStatus = message.data;
}
this.loading = false;
loadInit$.unsubscribe();
},
error => {
this.loading = false;
this.notifySvc.error(error.msg, '');
loadInit$.unsubscribe();
let loadInit$ = this.statusPagePublicService.getStatusPageOrg().subscribe(
(message: Message<StatusPageOrg>) => {
if (message.code === 0) {
this.statusOrg = message.data;
this.titleService.setTitle(`${this.statusOrg.name} ${this.i18nSvc.fanyi('menu.advanced.status')}`);
} else {
this.statusOrg = new StatusPageOrg();
console.log(message.msg);
this.notifySvc.error(message.msg, '');
}
);
this.loadComponentStatus();
loadInit$.unsubscribe();
},
error => {
this.loading = false;
this.notifySvc.error(error.msg, '');
loadInit$.unsubscribe();
}
);
}
showIncident() {
@@ -43,12 +43,22 @@ export class StatusPagePublicService {
return this.http.get<Message<StatusPageOrg>>(status_page_org_public_uri);
}
public getStatusPageComponents(): Observable<Message<StatusPageComponentStatus[]>> {
return this.http.get<Message<StatusPageComponentStatus[]>>(status_page_component_public_uri);
public getStatusPageComponents(days?: number): Observable<Message<StatusPageComponentStatus[]>> {
let httpParams = new HttpParams();
if (days != null) {
httpParams = httpParams.append('days', days);
}
const options = { params: httpParams };
return this.http.get<Message<StatusPageComponentStatus[]>>(status_page_component_public_uri, options);
}
public getStatusPageComponent(componentId: number): Observable<Message<StatusPageComponentStatus>> {
return this.http.get<Message<StatusPageComponentStatus>>(`${status_page_component_public_uri}/${componentId}`);
public getStatusPageComponent(componentId: number, days?: number): Observable<Message<StatusPageComponentStatus>> {
let httpParams = new HttpParams();
if (days != null) {
httpParams = httpParams.append('days', days);
}
const options = { params: httpParams };
return this.http.get<Message<StatusPageComponentStatus>>(`${status_page_component_public_uri}/${componentId}`, options);
}
public getStatusPageIncidents(
+4
View File
@@ -1171,6 +1171,10 @@
"status.preview": "View Status Page",
"status.page": "Status Page",
"status.public.30-day": "30 days ago",
"status.public.24-hour": "24 hours",
"status.public.24-hour-ago": "24 hours ago",
"status.public.days": "days",
"status.public.days-ago": "days ago",
"status.public.feedback": "Feedback Issue",
"status.public.org.state.0": "All Systems Operational",
"status.public.org.state.1": "Some Systems Abnormal",
+4
View File
@@ -1090,6 +1090,10 @@
"status.preview": "ステータスページを表示",
"status.page": "ステータスページ",
"status.public.30-day": "30日前",
"status.public.24-hour": "24時間",
"status.public.24-hour-ago": "24時間前",
"status.public.days": "日",
"status.public.days-ago": "日前",
"status.public.feedback": "フィードバック問題",
"status.public.org.state.0": "すべてのシステムが正常に稼働",
"status.public.org.state.1": "一部のシステムが異常",
+4
View File
@@ -1055,6 +1055,10 @@
"status.preview": "상태 페이지 보기",
"status.page": "상태 페이지",
"status.public.30-day": "30일 전",
"status.public.24-hour": "24시간",
"status.public.24-hour-ago": "24시간 전",
"status.public.days": "일",
"status.public.days-ago": "일 전",
"status.public.feedback": "피드백 제출",
"status.public.org.state.0": "모든 시스템 정상 운영 중",
"status.public.org.state.1": "일부 시스템 비정상",
+4
View File
@@ -1071,6 +1071,10 @@
"status.public.org.state.2": "Todos os Sistemas Anormais",
"status.public.today": "Hoje",
"status.public.30-day": "30 dias atrás",
"status.public.24-hour": "24 horas",
"status.public.24-hour-ago": "24 horas atrás",
"status.public.days": "dias",
"status.public.days-ago": "dias atrás",
"status.public.power-by": "Desenvolvido por Apache HertzBeat™. Dê-nos uma estrela!",
"status.public.to-incident": "Histórico de Incidentes",
"status.public.to-component": "Página de Status",
+4
View File
@@ -1174,6 +1174,10 @@
"status.preview": "查看状态页",
"status.page": "状态页面",
"status.public.30-day": "30天前",
"status.public.24-hour": "24小时",
"status.public.24-hour-ago": "24小时前",
"status.public.days": "天",
"status.public.days-ago": "天前",
"status.public.feedback": "问题反馈",
"status.public.org.state.0": "所有系统正常运行",
"status.public.org.state.1": "部分系统运行异常",
+4
View File
@@ -1104,6 +1104,10 @@
"status.preview": "查看狀態頁",
"status.page": "狀態頁面",
"status.public.30-day": "30天前",
"status.public.24-hour": "24小時",
"status.public.24-hour-ago": "24小時前",
"status.public.days": "天",
"status.public.days-ago": "天前",
"status.public.feedback": "問題反饋",
"status.public.org.state.0": "所有系統正常運行",
"status.public.org.state.1": "部分系統運行異常",