diff --git a/src/main/java/de/tum/cit/ase/domain/LocalCIStatus.java b/src/main/java/de/tum/cit/ase/domain/LocalCIStatus.java new file mode 100644 index 00000000..a3d0387b --- /dev/null +++ b/src/main/java/de/tum/cit/ase/domain/LocalCIStatus.java @@ -0,0 +1,89 @@ +package de.tum.cit.ase.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; + +@Entity +@Table(name = "local_ci_status") +public class LocalCIStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "is_finished") + private boolean isFinished; + + @Column(name = "queued_jobs") + private int queuedJobs; + + @Column(name = "total_jobs") + private int totalJobs; + + @Column(name = "time_in_minutes") + private int timeInMinutes; + + @Column(name = "avg_jobs_per_minute") + private double avgJobsPerMinute; + + @OneToOne + @JoinColumn(name = "simulation_run_id", nullable = false) + @JsonIgnore + private SimulationRun simulationRun; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public boolean isFinished() { + return isFinished; + } + + public void setFinished(boolean finished) { + isFinished = finished; + } + + public int getQueuedJobs() { + return queuedJobs; + } + + public void setQueuedJobs(int queuedJobs) { + this.queuedJobs = queuedJobs; + } + + public int getTotalJobs() { + return totalJobs; + } + + public void setTotalJobs(int totalJobs) { + this.totalJobs = totalJobs; + } + + public int getTimeInMinutes() { + return timeInMinutes; + } + + public void setTimeInMinutes(int timeInMinutes) { + this.timeInMinutes = timeInMinutes; + } + + public double getAvgJobsPerMinute() { + return avgJobsPerMinute; + } + + public void setAvgJobsPerMinute(double avgJobsPerMinute) { + this.avgJobsPerMinute = avgJobsPerMinute; + } + + public SimulationRun getSimulationRun() { + return simulationRun; + } + + public void setSimulationRun(SimulationRun simulationRun) { + this.simulationRun = simulationRun; + } +} diff --git a/src/main/java/de/tum/cit/ase/domain/SimulationRun.java b/src/main/java/de/tum/cit/ase/domain/SimulationRun.java index b4131dbc..7d89a43a 100644 --- a/src/main/java/de/tum/cit/ase/domain/SimulationRun.java +++ b/src/main/java/de/tum/cit/ase/domain/SimulationRun.java @@ -35,6 +35,9 @@ public class SimulationRun { @OneToMany(cascade = CascadeType.REMOVE, mappedBy = "simulationRun") private Set logMessages; + @OneToOne(cascade = CascadeType.REMOVE, mappedBy = "simulationRun", fetch = FetchType.EAGER) + private LocalCIStatus localCIStatus; + @Transient private ArtemisAccountDTO adminAccount; @@ -113,6 +116,14 @@ public void setEndDateTime(ZonedDateTime endDateTime) { this.endDateTime = endDateTime; } + public LocalCIStatus getLocalCIStatus() { + return localCIStatus; + } + + public void setLocalCIStatus(LocalCIStatus localCIStatus) { + this.localCIStatus = localCIStatus; + } + public enum Status { QUEUED, RUNNING, diff --git a/src/main/java/de/tum/cit/ase/repository/LocalCIStatusRepository.java b/src/main/java/de/tum/cit/ase/repository/LocalCIStatusRepository.java new file mode 100644 index 00000000..ce593e61 --- /dev/null +++ b/src/main/java/de/tum/cit/ase/repository/LocalCIStatusRepository.java @@ -0,0 +1,16 @@ +package de.tum.cit.ase.repository; + +import de.tum.cit.ase.domain.LocalCIStatus; +import jakarta.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface LocalCIStatusRepository extends JpaRepository { + @Modifying + @Transactional + @Query(value = "delete from LocalCIStatus status where status.isFinished = false") + void deleteAllNotFinished(); +} diff --git a/src/main/java/de/tum/cit/ase/repository/SimulationRunRepository.java b/src/main/java/de/tum/cit/ase/repository/SimulationRunRepository.java index 34a572ea..a879fcd8 100644 --- a/src/main/java/de/tum/cit/ase/repository/SimulationRunRepository.java +++ b/src/main/java/de/tum/cit/ase/repository/SimulationRunRepository.java @@ -2,6 +2,7 @@ import de.tum.cit.ase.domain.SimulationRun; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -16,5 +17,5 @@ public interface SimulationRunRepository extends JpaRepository findAllBySimulationId(@Param("simulationId") long simulationId); @Query(value = "select run from SimulationRun run left join fetch run.stats s left join fetch run.logMessages l where run.id = :#{#id}") - SimulationRun findByIdWithStatsAndLogMessages(long id); + Optional findByIdWithStatsAndLogMessages(long id); } diff --git a/src/main/java/de/tum/cit/ase/service/LocalCIStatusService.java b/src/main/java/de/tum/cit/ase/service/LocalCIStatusService.java new file mode 100644 index 00000000..83254517 --- /dev/null +++ b/src/main/java/de/tum/cit/ase/service/LocalCIStatusService.java @@ -0,0 +1,72 @@ +package de.tum.cit.ase.service; + +import de.tum.cit.ase.domain.LocalCIStatus; +import de.tum.cit.ase.domain.SimulationRun; +import de.tum.cit.ase.repository.LocalCIStatusRepository; +import de.tum.cit.ase.service.artemis.interaction.SimulatedArtemisAdmin; +import de.tum.cit.ase.web.websocket.SimulationWebsocketService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +public class LocalCIStatusService { + + private final Logger log = LoggerFactory.getLogger(LocalCIStatusService.class); + private final LocalCIStatusRepository localCIStatusRepository; + private final SimulationWebsocketService websocketService; + + public LocalCIStatusService(LocalCIStatusRepository localCIStatusRepository, SimulationWebsocketService websocketService) { + this.localCIStatusRepository = localCIStatusRepository; + this.websocketService = websocketService; + cleanup(); + } + + public LocalCIStatus createLocalCIStatus(SimulationRun simulationRun) { + LocalCIStatus status = new LocalCIStatus(); + status.setSimulationRun(simulationRun); + status.setFinished(false); + status.setAvgJobsPerMinute(0); + status.setQueuedJobs(0); + status.setTotalJobs(0); + status.setTimeInMinutes(0); + return localCIStatusRepository.save(status); + } + + @Async + public void subscribeToLocalCIStatus(SimulationRun simulationRun, SimulatedArtemisAdmin admin, long courseId) { + log.info("Subscribing to local CI status for simulation run {}", simulationRun.getId()); + LocalCIStatus status = createLocalCIStatus(simulationRun); + + int numberOfQueuedJobs = admin.getBuildQueue(courseId).size(); + status.setTotalJobs(numberOfQueuedJobs); + status.setQueuedJobs(numberOfQueuedJobs); + status = localCIStatusRepository.save(status); + websocketService.sendRunLocalCIUpdate(simulationRun.getId(), status); + + do { + try { + Thread.sleep(1000 * 60); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + log.debug("Updating local CI status for simulation run {}", simulationRun.getId()); + numberOfQueuedJobs = admin.getBuildQueue(courseId).size(); + status.setQueuedJobs(numberOfQueuedJobs); + status.setTimeInMinutes(status.getTimeInMinutes() + 1); + status.setAvgJobsPerMinute((double) (status.getTotalJobs() - status.getQueuedJobs()) / status.getTimeInMinutes()); + status = localCIStatusRepository.save(status); + websocketService.sendRunLocalCIUpdate(simulationRun.getId(), status); + } while (numberOfQueuedJobs > 0); + status.setFinished(true); + status = localCIStatusRepository.save(status); + websocketService.sendRunLocalCIUpdate(simulationRun.getId(), status); + log.info("Finished subscribing to local CI status for simulation run {}", simulationRun.getId()); + } + + private void cleanup() { + log.info("Cleaning up local CI status"); + localCIStatusRepository.deleteAllNotFinished(); + } +} diff --git a/src/main/java/de/tum/cit/ase/service/PrometheusService.java b/src/main/java/de/tum/cit/ase/service/PrometheusService.java index d55d27b0..cd768cd9 100644 --- a/src/main/java/de/tum/cit/ase/service/PrometheusService.java +++ b/src/main/java/de/tum/cit/ase/service/PrometheusService.java @@ -77,7 +77,7 @@ public List getCpuUsageVcs(SimulationRun run) { * @return A list of CPU usage values. An empty list if no Prometheus instance is configured for the CI system. */ public List getCpuUsageCi(SimulationRun run) { - log.info("Getting CI CPU usage for {}", run); + log.debug("Getting CI CPU usage for {}", run); var instance = artemisConfiguration.getPrometheusInstanceCi(run.getSimulation().getServer()); if (instance == null || instance.isBlank()) { log.warn("No Prometheus instance configured for CI on {}", run.getSimulation().getServer()); @@ -94,7 +94,7 @@ public List getCpuUsageCi(SimulationRun run) { * @return The response from Prometheus. */ public QueryResponse executeQuery(String query, ZonedDateTime start, ZonedDateTime end) { - log.info("Querying Prometheus: {}", query); + log.debug("Querying Prometheus: {}", query); if (webClient == null) { setupWebclient(); } diff --git a/src/main/java/de/tum/cit/ase/service/artemis/ArtemisConfiguration.java b/src/main/java/de/tum/cit/ase/service/artemis/ArtemisConfiguration.java index d9f7046c..9ba4eb8b 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/ArtemisConfiguration.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/ArtemisConfiguration.java @@ -22,6 +22,9 @@ public class ArtemisConfiguration { @Value("${artemis.local.prometheus-instances.ci}") private String localPrometheusInstanceCi; + @Value("${artemis.local.is-local}") + private boolean localIsLocal; + @Value("${artemis.ts1.url}") private String test1Url; @@ -37,6 +40,9 @@ public class ArtemisConfiguration { @Value("${artemis.ts1.prometheus-instances.ci}") private String test1PrometheusInstanceCi; + @Value("${artemis.ts1.is-local}") + private boolean test1IsLocal; + @Value("${artemis.ts3.url}") private String test3Url; @@ -52,6 +58,9 @@ public class ArtemisConfiguration { @Value("${artemis.ts3.prometheus-instances.ci}") private String test3PrometheusInstanceCi; + @Value("${artemis.ts3.is-local}") + private boolean test3IsLocal; + @Value("${artemis.staging.url}") private String stagingUrl; @@ -67,6 +76,9 @@ public class ArtemisConfiguration { @Value("${artemis.staging.prometheus-instances.ci}") private String stagingPrometheusInstanceCi; + @Value("${artemis.staging.is-local}") + private boolean stagingIsLocal; + @Value("${artemis.staging2.url}") private String staging2Url; @@ -82,6 +94,9 @@ public class ArtemisConfiguration { @Value("${artemis.staging2.prometheus-instances.ci}") private String staging2PrometheusInstanceCi; + @Value("${artemis.staging2.is-local}") + private boolean staging2IsLocal; + @Value("${artemis.production.url}") private String productionUrl; @@ -97,6 +112,9 @@ public class ArtemisConfiguration { @Value("${artemis.production.prometheus-instances.ci}") private String productionPrometheusInstanceCi; + @Value("${artemis.production.is-local}") + private boolean productionIsLocal; + public String getUrl(ArtemisServer server) { return switch (server) { case LOCAL -> localUrl; @@ -151,4 +169,15 @@ public String getPrometheusInstanceCi(ArtemisServer server) { case PRODUCTION -> productionPrometheusInstanceCi; }; } + + public boolean getIsLocal(ArtemisServer server) { + return switch (server) { + case LOCAL -> localIsLocal; + case TS1 -> test1IsLocal; + case TS3 -> test3IsLocal; + case STAGING -> stagingIsLocal; + case STAGING2 -> staging2IsLocal; + case PRODUCTION -> productionIsLocal; + }; + } } diff --git a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java index 0f5f7d2f..5be91854 100644 --- a/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java +++ b/src/main/java/de/tum/cit/ase/service/artemis/interaction/SimulatedArtemisAdmin.java @@ -10,6 +10,7 @@ import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.schedulers.Schedulers; import java.time.ZonedDateTime; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.slf4j.LoggerFactory; @@ -463,4 +464,14 @@ public void deleteExam(long courseId, long examId) { .toBodilessEntity() .block(); } + + public List getBuildQueue(long courseId) { + return webClient + .get() + .uri(uriBuilder -> uriBuilder.pathSegment("api", "build-job-queue", "queued", String.valueOf(courseId)).build()) + .retrieve() + .bodyToFlux(DomainObject.class) + .collectList() + .block(); + } } diff --git a/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java b/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java index d4629fbf..e63844b1 100644 --- a/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java +++ b/src/main/java/de/tum/cit/ase/service/simulation/SimulationDataService.java @@ -77,7 +77,7 @@ public SimulationRun getSimulationRun(long id) { } public SimulationRun getSimulationRunWithStatsAndLogs(long id) { - return simulationRunRepository.findByIdWithStatsAndLogMessages(id); + return simulationRunRepository.findByIdWithStatsAndLogMessages(id).orElseThrow(); } public void deleteSimulation(long id) { @@ -166,7 +166,7 @@ public void cancelActiveRun(long runId) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - run = simulationRunRepository.findById(runId).orElseThrow(); + run = simulationRunRepository.findByIdWithStatsAndLogMessages(runId).orElseThrow(); run.setStatus(SimulationRun.Status.CANCELLED); run.setEndDateTime(now()); diff --git a/src/main/java/de/tum/cit/ase/service/simulation/SimulationRunExecutionService.java b/src/main/java/de/tum/cit/ase/service/simulation/SimulationRunExecutionService.java index 5e14351f..c0d4a7b1 100644 --- a/src/main/java/de/tum/cit/ase/service/simulation/SimulationRunExecutionService.java +++ b/src/main/java/de/tum/cit/ase/service/simulation/SimulationRunExecutionService.java @@ -7,6 +7,7 @@ import de.tum.cit.ase.domain.*; import de.tum.cit.ase.repository.LogMessageRepository; import de.tum.cit.ase.repository.SimulationRunRepository; +import de.tum.cit.ase.service.LocalCIStatusService; import de.tum.cit.ase.service.MailService; import de.tum.cit.ase.service.artemis.ArtemisConfiguration; import de.tum.cit.ase.service.artemis.ArtemisUserService; @@ -40,6 +41,7 @@ public class SimulationRunExecutionService { private final SimulationResultService simulationResultService; private final LogMessageRepository logMessageRepository; private final MailService mailService; + private final LocalCIStatusService localCIStatusService; private boolean doNotSleep = false; public SimulationRunExecutionService( @@ -49,7 +51,8 @@ public SimulationRunExecutionService( SimulationRunRepository simulationRunRepository, SimulationResultService simulationResultService, LogMessageRepository logMessageRepository, - MailService mailService + MailService mailService, + LocalCIStatusService localCIStatusService ) { this.simulationWebsocketService = simulationWebsocketService; this.artemisConfiguration = artemisConfiguration; @@ -58,6 +61,7 @@ public SimulationRunExecutionService( this.logMessageRepository = logMessageRepository; this.artemisUserService = artemisUserService; this.mailService = mailService; + this.localCIStatusService = localCIStatusService; } /** @@ -146,7 +150,7 @@ public synchronized void simulateExam(SimulationRun simulationRun) { return; } - if (!doNotSleep) { + if (!doNotSleep && !artemisConfiguration.getIsLocal(simulationRun.getSimulation().getServer())) { // Wait for synchronization of user groups try { logAndSend(false, simulationRun, "Waiting for synchronization of user groups (1 min)..."); @@ -280,6 +284,17 @@ public synchronized void simulateExam(SimulationRun simulationRun) { SimulationRun runWithResult = simulationResultService.calculateAndSaveResult(simulationRun, requestStats); finishSimulationRun(runWithResult); sendRunResult(runWithResult); + if (artemisConfiguration.getIsLocal(simulationRun.getSimulation().getServer())) { + if (admin == null) { + try { + admin = initializeAdmin(simulationRun.getSimulation().getServer()); + } catch (Exception e) { + logAndSend(true, simulationRun, "Cannot get local-ci status, no admin account available."); + return; + } + } + localCIStatusService.subscribeToLocalCIStatus(runWithResult, admin, courseId); + } } /** diff --git a/src/main/java/de/tum/cit/ase/web/rest/PrometheusResource.java b/src/main/java/de/tum/cit/ase/web/rest/PrometheusResource.java index e505f0e8..5df2cb0c 100644 --- a/src/main/java/de/tum/cit/ase/web/rest/PrometheusResource.java +++ b/src/main/java/de/tum/cit/ase/web/rest/PrometheusResource.java @@ -4,6 +4,7 @@ import de.tum.cit.ase.prometheus.MetricValue; import de.tum.cit.ase.security.AuthoritiesConstants; import de.tum.cit.ase.service.PrometheusService; +import de.tum.cit.ase.service.artemis.ArtemisConfiguration; import de.tum.cit.ase.service.simulation.SimulationDataService; import java.util.List; import org.springframework.http.ResponseEntity; @@ -20,10 +21,16 @@ public class PrometheusResource { private final PrometheusService prometheusService; private final SimulationDataService simulationDataService; + private final ArtemisConfiguration artemisConfiguration; - public PrometheusResource(PrometheusService prometheusService, SimulationDataService simulationDataService) { + public PrometheusResource( + PrometheusService prometheusService, + SimulationDataService simulationDataService, + ArtemisConfiguration artemisConfiguration + ) { this.simulationDataService = simulationDataService; this.prometheusService = prometheusService; + this.artemisConfiguration = artemisConfiguration; } /** @@ -51,6 +58,9 @@ public ResponseEntity> getCpuUsageVcs(@PathVariable("runId") l if (run.getStatus() != SimulationRun.Status.FINISHED && run.getStatus() != SimulationRun.Status.RUNNING) { return ResponseEntity.ok(List.of()); } + if (artemisConfiguration.getIsLocal(run.getSimulation().getServer())) { + return ResponseEntity.ok(List.of()); + } return ResponseEntity.ok(prometheusService.getCpuUsageVcs(run)); } @@ -65,6 +75,9 @@ public ResponseEntity> getCpuUsageCi(@PathVariable("runId") lo if (run.getStatus() != SimulationRun.Status.FINISHED && run.getStatus() != SimulationRun.Status.RUNNING) { return ResponseEntity.ok(List.of()); } + if (artemisConfiguration.getIsLocal(run.getSimulation().getServer())) { + return ResponseEntity.ok(List.of()); + } return ResponseEntity.ok(prometheusService.getCpuUsageCi(run)); } } diff --git a/src/main/java/de/tum/cit/ase/web/websocket/SimulationWebsocketService.java b/src/main/java/de/tum/cit/ase/web/websocket/SimulationWebsocketService.java index 8e51e2c9..b7e10c3b 100644 --- a/src/main/java/de/tum/cit/ase/web/websocket/SimulationWebsocketService.java +++ b/src/main/java/de/tum/cit/ase/web/websocket/SimulationWebsocketService.java @@ -1,5 +1,6 @@ package de.tum.cit.ase.web.websocket; +import de.tum.cit.ase.domain.LocalCIStatus; import de.tum.cit.ase.domain.LogMessage; import de.tum.cit.ase.domain.SimulationRun; import org.springframework.messaging.simp.SimpMessageSendingOperations; @@ -12,6 +13,7 @@ public class SimulationWebsocketService { private static final String TOPIC_RUN_STATUS_UPDATE = "/topic/simulation/runs/%d/status"; private static final String TOPIC_RUN_LOG_MESSAGE = "/topic/simulation/runs/%d/log"; private static final String TOPIC_NEW_RUN = "/topic/simulation/%d/runs/new"; + private static final String TOPIC_RUN_LOCAL_CI_UPDATE = "/topic/simulation/runs/%d/local-ci-status"; private final SimpMessageSendingOperations messagingTemplate; @@ -47,4 +49,8 @@ public void sendRunLogMessage(SimulationRun run, LogMessage logMessage) { public void sendNewRun(SimulationRun run) { messagingTemplate.convertAndSend(String.format(TOPIC_NEW_RUN, run.getSimulation().getId()), run); } + + public void sendRunLocalCIUpdate(long runId, LocalCIStatus status) { + messagingTemplate.convertAndSend(String.format(TOPIC_RUN_LOCAL_CI_UPDATE, runId), status); + } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 67769fd8..d9c50436 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -214,6 +214,7 @@ artemis: local: url: http://localhost:8080/ cleanup-enabled: true + is-local: true prometheus-instances: artemis: vcs: @@ -221,6 +222,7 @@ artemis: ts3: url: https://artemis-test3.artemis.cit.tum.de/ cleanup-enabled: false + is-local: true prometheus-instances: artemis: artemis-test3.artemis.cit.tum.de:9100 vcs: @@ -228,6 +230,7 @@ artemis: ts1: url: https://artemis-test1.artemis.cit.tum.de/ cleanup-enabled: false + is-local: false prometheus-instances: artemis: artemis-test1.artemis.cit.tum.de:9100 vcs: bitbucket-prelive-node1.ase.in.tum.de:9100 @@ -235,6 +238,7 @@ artemis: staging: url: https://artemis-staging.artemis.in.tum.de/ cleanup-enabled: false + is-local: false prometheus-instances: artemis: vcs: bitbucket-prelive-node1.ase.in.tum.de:9100 @@ -242,6 +246,7 @@ artemis: staging2: url: https://artemis-staging-localci.artemis.cit.tum.de/ cleanup-enabled: false + is-local: true prometheus-instances: artemis: vcs: @@ -249,6 +254,7 @@ artemis: production: url: https://artemis.cit.tum.de/ cleanup-enabled: false + is-local: false prometheus-instances: artemis: vcs: bitbucket-node1.ase.in.tum.de:9100 diff --git a/src/main/resources/config/liquibase/changelog/00000000000015_add_local_ci_status.xml b/src/main/resources/config/liquibase/changelog/00000000000015_add_local_ci_status.xml new file mode 100644 index 00000000..99ce268c --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000015_add_local_ci_status.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 74ecd91b..453fb47e 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -23,6 +23,7 @@ + diff --git a/src/main/webapp/app/app.module.ts b/src/main/webapp/app/app.module.ts index ea246564..b89528ad 100644 --- a/src/main/webapp/app/app.module.ts +++ b/src/main/webapp/app/app.module.ts @@ -28,6 +28,7 @@ import { CreateSimulationBoxComponent } from './layouts/create-simulation-box/cr import { ServerBadgeComponent } from './layouts/server-badge/server-badge.component'; import { ModeExplanationComponent } from './layouts/mode-explanation/mode-explanation.component'; import { PrometheusBoxComponent } from './layouts/prometheus-box/prometheus-box.component'; +import { LocalCiStatusCardComponent } from './layouts/local-ci-status-card/local-ci-status-card.component'; // jhipster-needle-angular-add-module-import JHipster will add new module here @NgModule({ @@ -44,6 +45,7 @@ import { PrometheusBoxComponent } from './layouts/prometheus-box/prometheus-box. FontAwesomeModule, NgbAccordionModule, PrometheusBoxComponent, + LocalCiStatusCardComponent, ], providers: [ Title, diff --git a/src/main/webapp/app/entities/simulation/localCIStatus.ts b/src/main/webapp/app/entities/simulation/localCIStatus.ts new file mode 100644 index 00000000..efc6392a --- /dev/null +++ b/src/main/webapp/app/entities/simulation/localCIStatus.ts @@ -0,0 +1,13 @@ +import { SimulationRun } from './simulationRun'; + +export class LocalCIStatus { + constructor( + public id: number, + public finished: boolean, + public queuedJobs: number, + public totalJobs: number, + public timeInMinutes: number, + public avgJobsPerMinute: number, + public simulationRun: SimulationRun, + ) {} +} diff --git a/src/main/webapp/app/entities/simulation/simulationRun.ts b/src/main/webapp/app/entities/simulation/simulationRun.ts index b194b3af..0c8956f0 100644 --- a/src/main/webapp/app/entities/simulation/simulationRun.ts +++ b/src/main/webapp/app/entities/simulation/simulationRun.ts @@ -1,6 +1,7 @@ import { SimulationStats } from './simulationStats'; import { Simulation } from './simulation'; import { LogMessage } from './logMessage'; +import { LocalCIStatus } from './localCIStatus'; export class SimulationRun { constructor( @@ -11,6 +12,7 @@ export class SimulationRun { public simulation: Simulation, public logMessages: LogMessage[], public endDateTime?: Date, + public localCIStatus?: LocalCIStatus, ) {} } diff --git a/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.html b/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.html new file mode 100644 index 00000000..7fffe3db --- /dev/null +++ b/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.html @@ -0,0 +1,19 @@ +
+
+ @if (localCIStatus.finished) { + All jobs finished. +
+

Total jobs: {{ localCIStatus.totalJobs }}

+

Executing jobs took {{ localCIStatus.timeInMinutes }} minutes

+
+ } @else { + Jobs running... +
+

Finished: {{ localCIStatus.totalJobs - localCIStatus.queuedJobs }} / {{ localCIStatus.totalJobs }}

+

Queued: {{ localCIStatus.queuedJobs }}

+

Running for {{ localCIStatus.timeInMinutes }} minutes

+
+ } +

Average jobs per minute: {{ localCIStatus.avgJobsPerMinute | number: '1.0-1' }}

+
+
diff --git a/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.scss b/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.ts b/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.ts new file mode 100644 index 00000000..1c9f04fe --- /dev/null +++ b/src/main/webapp/app/layouts/local-ci-status-card/local-ci-status-card.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +import { LocalCIStatus } from '../../entities/simulation/localCIStatus'; +import { DecimalPipe } from '@angular/common'; + +@Component({ + selector: 'jhi-local-ci-status-card', + standalone: true, + imports: [DecimalPipe], + templateUrl: './local-ci-status-card.component.html', + styleUrl: './local-ci-status-card.component.scss', +}) +export class LocalCiStatusCardComponent { + @Input() localCIStatus!: LocalCIStatus; +} diff --git a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html index db6a6f40..8bab3173 100644 --- a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html +++ b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.html @@ -52,20 +52,53 @@

}

-
-
-

- -

-
-
- - - + @if (!selectedRun.localCIStatus) { +
+
+

+ +

+
+
+ + + +
-
+ } @else { +
+
+
+

+ +

+
+
+ + + +
+
+
+
+
+
+

+ +

+
+
+ + + +
+
+
+
+
+ }

diff --git a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.scss b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.scss index e69de29b..32b0985c 100644 --- a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.scss +++ b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.scss @@ -0,0 +1,12 @@ +.accordion-wrapper { + @media (min-width: 1200px) { + display: flex; + .log-accordion { + width: 75%; + margin-right: 10px; + } + .status-accordion { + width: 25%; + } + } +} diff --git a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.ts b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.ts index dfd04ae5..5ad8795a 100644 --- a/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.ts +++ b/src/main/webapp/app/simulations/simulations-overview/simulations-overview.component.ts @@ -127,5 +127,8 @@ export class SimulationsOverviewComponent implements OnInit { this.simulationsService.receiveSimulationResult(run).subscribe(stats => { run.stats = stats.sort((a, b) => getOrder(a) - getOrder(b)); }); + this.simulationsService.receiveLocalCIStatus(run).subscribe(localCIStatus => { + run.localCIStatus = localCIStatus; + }); } } diff --git a/src/main/webapp/app/simulations/simulations.service.ts b/src/main/webapp/app/simulations/simulations.service.ts index 6daccfda..e22eab79 100644 --- a/src/main/webapp/app/simulations/simulations.service.ts +++ b/src/main/webapp/app/simulations/simulations.service.ts @@ -11,6 +11,7 @@ import { SimulationStats } from '../entities/simulation/simulationStats'; import { LogMessage } from '../entities/simulation/logMessage'; import { ArtemisServer } from '../core/util/artemisServer'; import { SimulationSchedule } from '../entities/simulation/simulationSchedule'; +import { LocalCIStatus } from '../entities/simulation/localCIStatus'; @Injectable({ providedIn: 'root', @@ -42,6 +43,13 @@ export class SimulationsService { return this.websocketService.receive('/topic/simulation/' + simulation.id + '/runs/new').pipe(map((res: any) => res as SimulationRun)); } + receiveLocalCIStatus(simulationRun: SimulationRun): Observable { + this.websocketService.subscribe('/topic/simulation/runs/' + simulationRun.id + '/local-ci-status'); + return this.websocketService + .receive('/topic/simulation/runs/' + simulationRun.id + '/local-ci-status') + .pipe(map((res: any) => res as LocalCIStatus)); + } + createSimulation(simulation: Simulation): Observable { const endpoint = this.applicationConfigService.getEndpointFor('/api/simulations'); return this.httpClient.post(endpoint, simulation).pipe(map((res: any) => res as Simulation)); @@ -125,6 +133,7 @@ export class SimulationsService { public unsubscribeFromSelectedSimulationRun(run: SimulationRun): void { this.websocketService.unsubscribe('/topic/simulation/runs/' + run.id + '/result'); this.websocketService.unsubscribe('/topic/simulation/runs/' + run.id + '/log'); + this.websocketService.unsubscribe('/topic/simulation/runs/' + run.id + '/local-ci-status'); } public unsubscribeFromSimulationRun(run: SimulationRun): void { diff --git a/src/test/java/de/tum/cit/ase/service/SimulationDataServiceIT.java b/src/test/java/de/tum/cit/ase/service/SimulationDataServiceIT.java index eb21b7c6..db719fdc 100644 --- a/src/test/java/de/tum/cit/ase/service/SimulationDataServiceIT.java +++ b/src/test/java/de/tum/cit/ase/service/SimulationDataServiceIT.java @@ -416,6 +416,7 @@ public void cancelActiveRun_success() { run.setLogMessages(new HashSet<>()); run.setStatus(SimulationRun.Status.RUNNING); + when(simulationRunRepository.findByIdWithStatsAndLogMessages(1L)).thenReturn(java.util.Optional.of(run)); when(simulationRunRepository.findById(1L)).thenReturn(java.util.Optional.of(run)); doNothing().when(simulationRunQueueService).abortSimulationExecution(); doNothing().when(simulationWebsocketService).sendRunStatusUpdate(any()); diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 62a626c1..6433a53f 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -93,6 +93,7 @@ artemis: local: url: cleanup-enabled: false + is-local: true prometheus-instances: artemis: vcs: @@ -100,6 +101,7 @@ artemis: ts3: url: cleanup-enabled: false + is-local: true prometheus-instances: artemis: vcs: @@ -107,6 +109,7 @@ artemis: ts1: url: cleanup-enabled: false + is-local: false prometheus-instances: artemis: vcs: @@ -114,6 +117,7 @@ artemis: staging: url: cleanup-enabled: false + is-local: false prometheus-instances: artemis: vcs: @@ -121,6 +125,7 @@ artemis: staging2: url: cleanup-enabled: false + is-local: true prometheus-instances: artemis: vcs: @@ -128,6 +133,7 @@ artemis: production: url: cleanup-enabled: false + is-local: false prometheus-instances: artemis: vcs: