|
|
@@ -0,0 +1,351 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.cockpit.event.CockpitHistoryAnalysisAdapter;
|
|
|
+import com.zksy.cockpit.event.CockpitHistoryAnalysisResponse;
|
|
|
+import com.zksy.cockpit.event.CockpitHistoryAnalysisService;
|
|
|
+import com.zksy.cockpit.event.CockpitHistoryAnalysisSnapshot;
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
+import org.springframework.web.bind.annotation.DeleteMapping;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PatchMapping;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.PutMapping;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.hasSize;
|
|
|
+import static org.hamcrest.Matchers.nullValue;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
|
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
+
|
|
|
+class CockpitHistoryAnalysisControllerTest {
|
|
|
+
|
|
|
+ private static final String ENDPOINT = "/api/cockpit/v1/events/history-analysis";
|
|
|
+ private static final String START = "2026-09-01T23:00:00+08:00";
|
|
|
+ private static final String END = "2026-09-02T02:00:00+08:00";
|
|
|
+ private static final String BUCKET_23 = "2026-09-01T23:00:00+08:00";
|
|
|
+ private static final String BUCKET_00 = "2026-09-02T00:00:00+08:00";
|
|
|
+ private static final String BUCKET_01 = "2026-09-02T01:00:00+08:00";
|
|
|
+ private static final String BUCKET_02 = "2026-09-02T02:00:00+08:00";
|
|
|
+
|
|
|
+ private CockpitHistoryAnalysisAdapter adapter;
|
|
|
+ private MockMvc mockMvc;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ adapter = mock(CockpitHistoryAnalysisAdapter.class);
|
|
|
+ CockpitHistoryAnalysisService service = new CockpitHistoryAnalysisService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ mockMvc = MockMvcBuilders
|
|
|
+ .standaloneSetup(new CockpitHistoryAnalysisController(service))
|
|
|
+ .build();
|
|
|
+ when(adapter.load()).thenReturn(fixture());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void warningFamilyExposesSummaryRowsBucketsAndHandledScope() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END)
|
|
|
+ .header("X-Request-Id", "history-analysis-001"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("history-analysis-001"))
|
|
|
+ .andExpect(jsonPath("$.data.apiVersion").value("v1"))
|
|
|
+ .andExpect(jsonPath("$.data.readOnly").value(true))
|
|
|
+ .andExpect(jsonPath("$.data.displayName").value("历史分析"))
|
|
|
+ .andExpect(jsonPath("$.data.timezone").value("Asia/Shanghai"))
|
|
|
+ .andExpect(jsonPath("$.data.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.range.type").value("TREND_WINDOW"))
|
|
|
+ .andExpect(jsonPath("$.data.range.startTime").value(START))
|
|
|
+ .andExpect(jsonPath("$.data.range.endTime").value(END))
|
|
|
+ .andExpect(jsonPath("$.data.filters.eventFamily").value("WARNING"))
|
|
|
+ .andExpect(jsonPath("$.data.filters.level").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.filters.industry").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.eventCount").value(5))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.4000"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.rows", hasSize(4)))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].timeBucket").value(BUCKET_23))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].eventCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].handledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].handlingEfficiency.value").value("1.0000"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].mapFilter.deviceCodes[0]").value("D-GAS-1"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].timeBucket").value(BUCKET_00))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].handledCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].handlingEfficiency.value").value("0.0000"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].handlingEfficiency.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].timeBucket").value(BUCKET_01))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].handledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].handlingEfficiency.value").value("0.5000"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[3].timeBucket").value(BUCKET_02))
|
|
|
+ .andExpect(jsonPath("$.data.rows[3].eventCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.rows[3].handlingEfficiency.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[3].handlingEfficiency.reason").value("NO_EVENTS"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[3].handlingEfficiency.value").value("0.0000"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmFamilyIsIndependentFromWarnings() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].handledCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].eventCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].eventCount").value(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void asiaShanghaiHourBoundaryPlacesLateNightEventInPreviousBucket() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].timeBucket").value("2026-09-01T23:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].eventCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.rows[1].timeBucket").value("2026-09-02T00:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].timeBucket").value("2026-09-02T01:00:00+08:00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void handledScopeCountsOnlyClosedStateSixOrders() throws Exception {
|
|
|
+ CockpitHistoryAnalysisResponse response = service("WARNING", START, END);
|
|
|
+ assertEquals(5, response.getSummary().getEventCount());
|
|
|
+ assertEquals(2, response.getSummary().getHandledCount());
|
|
|
+ assertEquals("0.4000", response.getSummary().getHandlingEfficiency().getValue().toPlainString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void emptyTimeWindowReturnsSuccessfulAllZeroState() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", "2026-09-03T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-03T02:00:00+08:00"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.eventCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.reason").value("NO_EVENTS"))
|
|
|
+ .andExpect(jsonPath("$.data.rows", hasSize(3)))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].eventCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.rows[2].handlingEfficiency.state").value("EMPTY"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void defaultWindowCoversSevenDaysEndingAtBusinessTime() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.range.startTime").value("2026-08-26T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.range.endTime").value("2026-09-02T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.rows", hasSize(169)))
|
|
|
+ .andExpect(jsonPath("$.data.rows[0].timeBucket").value("2026-08-26T10:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.rows[168].timeBucket").value("2026-09-02T10:00:00+08:00"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void levelAndIndustryFiltersNarrowSummaryAndRows() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("level", "2")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.filters.level").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(0));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("industry", "GAS")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidEventFamilyLevelIndustryOrWindowReturnsTypedBadRequestBeforeAdapter() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.code").value(400))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "eventFamily must be WARNING or ALARM"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WHATEVER"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("level", "9"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "level must be an integer between 1 and 4"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("industry", "MANHOLE"))
|
|
|
+ .andExpect(status().isBadRequest());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("startTime", START))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "start time and end time must be provided together"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", END)
|
|
|
+ .param("endTime", START))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "start time must not be after end time"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", "2026-08-01T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-02T00:00:00+08:00"))
|
|
|
+ .andExpect(status().isBadRequest())
|
|
|
+ .andExpect(jsonPath("$.data.message").value(
|
|
|
+ "history window must not exceed 31 days"));
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void serverFailureReturnsTypedServerError() throws Exception {
|
|
|
+ when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .header("X-Request-Id", "history-analysis-500"))
|
|
|
+ .andExpect(status().isInternalServerError())
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
+ .andExpect(jsonPath("$.data.category").value("SERVER_ERROR"))
|
|
|
+ .andExpect(jsonPath("$.data.message").value("驾驶舱历史分析数据暂不可用"))
|
|
|
+ .andExpect(jsonPath("$.data.requestId").value("history-analysis-500"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void mutatingRequestIsRejectedBeforeReadAdapterIsCalled() throws Exception {
|
|
|
+ mockMvc.perform(post(ENDPOINT))
|
|
|
+ .andExpect(status().isMethodNotAllowed());
|
|
|
+
|
|
|
+ verifyNoInteractions(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void controllerOnlyExposesAuthenticatedReadOnlyOperations() {
|
|
|
+ assertTrue(Arrays.stream(CockpitHistoryAnalysisController.class.getDeclaredMethods())
|
|
|
+ .filter(this::hasRequestMappingAnnotation)
|
|
|
+ .allMatch(CockpitHistoryAnalysisControllerTest::isReadOnlyMappedMethod));
|
|
|
+
|
|
|
+ assertTrue(Arrays.stream(CockpitHistoryAnalysisController.class.getDeclaredMethods())
|
|
|
+ .anyMatch(method -> method.isAnnotationPresent(PreAuthorize.class)
|
|
|
+ && "isAuthenticated()".equals(
|
|
|
+ method.getAnnotation(PreAuthorize.class).value())));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasRequestMappingAnnotation(Method method) {
|
|
|
+ return method.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)
|
|
|
+ || method.isAnnotationPresent(GetMapping.class)
|
|
|
+ || method.isAnnotationPresent(PostMapping.class)
|
|
|
+ || method.isAnnotationPresent(PutMapping.class)
|
|
|
+ || method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ || method.isAnnotationPresent(DeleteMapping.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isReadOnlyMappedMethod(Method method) {
|
|
|
+ return method.isAnnotationPresent(GetMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PostMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PutMapping.class)
|
|
|
+ && !method.isAnnotationPresent(PatchMapping.class)
|
|
|
+ && !method.isAnnotationPresent(DeleteMapping.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private CockpitHistoryAnalysisResponse service(String family, String start, String end) {
|
|
|
+ return new CockpitHistoryAnalysisService(
|
|
|
+ adapter,
|
|
|
+ Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC))
|
|
|
+ .load("direct", family, null, null, start, end);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHistoryAnalysisSnapshot fixture() {
|
|
|
+ return new CockpitHistoryAnalysisSnapshot(
|
|
|
+ "2026-09-02T09:59:50+08:00",
|
|
|
+ Arrays.asList(
|
|
|
+ type("GAS_SENSOR", "GAS"),
|
|
|
+ type("WATER_SUPPLY_SENSOR", "WATER_SUPPLY"),
|
|
|
+ type("SEWAGE_ENV", "DRAINAGE"),
|
|
|
+ new CockpitHistoryAnalysisSnapshot.SourceType(
|
|
|
+ "WATER_PLANT", "水厂", "WATER_SUPPLY", "FACILITY"),
|
|
|
+ new CockpitHistoryAnalysisSnapshot.SourceType(
|
|
|
+ "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE")),
|
|
|
+ Arrays.asList(
|
|
|
+ warning("HW-001", 1, "2026-09-01T23:30:00+08:00", "GAS_SENSOR", "D-GAS-1"),
|
|
|
+ warning("HW-002", 2, "2026-09-02T00:15:00+08:00", "GAS_SENSOR", "D-GAS-2"),
|
|
|
+ warning("HW-003", 2, "2026-09-02T00:45:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-1"),
|
|
|
+ warning("HW-004", 4, "2026-09-02T01:20:00+08:00", "SEWAGE_ENV", "D-DR-1"),
|
|
|
+ warning("HW-005", 1, "2026-09-02T01:50:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-2"),
|
|
|
+ warning("HW-LEGACY", 1, "2026-09-02T00:10:00+08:00", "LEGACY", "D-LEGACY"),
|
|
|
+ warning("HW-MH", 1, "2026-09-01T23:59:00+08:00", "SMART_MANHOLE", "D-MH")),
|
|
|
+ Arrays.asList(
|
|
|
+ alarm("HA-001", 3, "2026-09-02T01:10:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-3"),
|
|
|
+ alarm("HA-002", 1, "2026-09-02T01:30:00+08:00", "GAS_SENSOR", "D-GAS-3")),
|
|
|
+ Arrays.asList(
|
|
|
+ workOrder("HW-001", 6),
|
|
|
+ workOrder("HW-002", 4),
|
|
|
+ workOrder("HW-003", 1),
|
|
|
+ workOrder("HW-004", 6),
|
|
|
+ workOrder("HW-005", 7),
|
|
|
+ workOrder("HA-001", 6)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHistoryAnalysisSnapshot.SourceType type(String code, String industry) {
|
|
|
+ return new CockpitHistoryAnalysisSnapshot.SourceType(
|
|
|
+ code, code, industry, "SMART_DEVICE");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHistoryAnalysisSnapshot.SourceWarning warning(
|
|
|
+ String no, Integer level, String eventTime, String typeCode, String deviceCode) {
|
|
|
+ return new CockpitHistoryAnalysisSnapshot.SourceWarning(
|
|
|
+ no, level, eventTime, typeCode, deviceCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHistoryAnalysisSnapshot.SourceAlarm alarm(
|
|
|
+ String id, Integer level, String eventTime, String typeCode, String deviceCode) {
|
|
|
+ return new CockpitHistoryAnalysisSnapshot.SourceAlarm(
|
|
|
+ id, level, eventTime, typeCode, deviceCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CockpitHistoryAnalysisSnapshot.SourceWorkOrderLink workOrder(
|
|
|
+ String eventNo, Integer stateCode) {
|
|
|
+ return new CockpitHistoryAnalysisSnapshot.SourceWorkOrderLink(eventNo, stateCode);
|
|
|
+ }
|
|
|
+}
|