mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
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:
co-authored by
Claude Opus 4.7
Duansg
Tomsun28
parent
032a4852f7
commit
4b217e7097
+9
-4
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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.
|
||||
|
||||
+18
-10
@@ -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()
|
||||
|
||||
+28
-2
@@ -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 {
|
||||
|
||||
|
||||
+5
-4
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "一部のシステムが異常",
|
||||
|
||||
@@ -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": "일부 시스템 비정상",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "部分系统运行异常",
|
||||
|
||||
@@ -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": "部分系統運行異常",
|
||||
|
||||
Reference in New Issue
Block a user