diff --git a/src/Application/Common/Interfaces/IJobService.cs b/src/Application/Common/Interfaces/IJobService.cs index 2e15b5777..f53750a92 100644 --- a/src/Application/Common/Interfaces/IJobService.cs +++ b/src/Application/Common/Interfaces/IJobService.cs @@ -8,4 +8,5 @@ public interface IJobService public void DeleteJob(string jobName); public string[] GetJobLogs(string jobName); public IEnumerable? GetJobs(); + public Job? GetJob(string jobName); } diff --git a/src/Application/JobStatuses/Queries/GetJobStatusQuery.cs b/src/Application/JobStatuses/Queries/GetJobStatusQuery.cs new file mode 100644 index 000000000..f704becfa --- /dev/null +++ b/src/Application/JobStatuses/Queries/GetJobStatusQuery.cs @@ -0,0 +1,31 @@ +using Hippo.Application.Common.Interfaces; +using Hippo.Application.Jobs; +using MediatR; + +namespace Hippo.Application.Channels.Queries; + +public class GetJobStatusQuery : IRequest +{ + public Guid ChannelId { get; set; } +} + +public class GetJobStatusQueryHandler : IRequestHandler +{ + private readonly IJobService _jobService; + + public GetJobStatusQueryHandler(IJobService jobService) + { + _jobService = jobService; + } + + public Task Handle(GetJobStatusQuery request, CancellationToken cancellationToken) + { + var job = _jobService.GetJob(request.ChannelId.ToString()); + + return Task.FromResult(new ChannelJobStatusItem + { + ChannelId = request.ChannelId, + Status = job?.Status ?? JobStatus.Dead, + }); + } +} diff --git a/src/Infrastructure/Services/NomadJobService.cs b/src/Infrastructure/Services/NomadJobService.cs index 28410845e..6bbf6c3e6 100644 --- a/src/Infrastructure/Services/NomadJobService.cs +++ b/src/Infrastructure/Services/NomadJobService.cs @@ -216,6 +216,24 @@ private Fermyon.Nomad.Model.Task GenerateJobTask(NomadJob nomadJob) } } + public Application.Jobs.Job? GetJob(string jobName) + { + try + { + var job = _jobsClient.GetJob(jobName); + + return new NomadJob(_configuration, + Guid.Parse(job.ID), + string.Empty, + string.Empty, + Enum.Parse(FormatNomadJobStatus(job.Status))); + } + catch + { + return null; + } + } + private string FormatNomadJobStatus(string status) { return char.ToUpper(status[0]) + status[1..]; diff --git a/src/Web/Api/JobStatusController.cs b/src/Web/Api/JobStatusController.cs index 9cf42b876..a89788c57 100644 --- a/src/Web/Api/JobStatusController.cs +++ b/src/Web/Api/JobStatusController.cs @@ -20,4 +20,13 @@ public async Task>> Index( PageSize = pageSize, }); } + + [HttpGet("{channelId}")] + public async Task> GetJobStatus(Guid channelId) + { + return await Mediator.Send(new GetJobStatusQuery + { + ChannelId = channelId, + }); + } } diff --git a/src/Web/ClientApp/.eslintignore b/src/Web/ClientApp/.eslintignore index 93299ae10..ab4be098e 100644 --- a/src/Web/ClientApp/.eslintignore +++ b/src/Web/ClientApp/.eslintignore @@ -5,4 +5,4 @@ e2e/** karma.conf.js commitlint.config.js *.spec.ts -src/app/core/api/* +src/app/core/api/** diff --git a/src/Web/ClientApp/src/app/components/channel/overview/overview.component.html b/src/Web/ClientApp/src/app/components/channel/overview/overview.component.html index 32d972887..5b78034b3 100644 --- a/src/Web/ClientApp/src/app/components/channel/overview/overview.component.html +++ b/src/Web/ClientApp/src/app/components/channel/overview/overview.component.html @@ -2,9 +2,13 @@
- - - + + + Version {{activeRevision.revisionNumber}} was published {{publishedAt}}.
diff --git a/src/Web/ClientApp/src/app/components/channel/overview/overview.component.ts b/src/Web/ClientApp/src/app/components/channel/overview/overview.component.ts index f41f20614..942736b67 100644 --- a/src/Web/ClientApp/src/app/components/channel/overview/overview.component.ts +++ b/src/Web/ClientApp/src/app/components/channel/overview/overview.component.ts @@ -1,7 +1,13 @@ -import { ChannelItem, ChannelService, RevisionItem } from 'src/app/core/api/v1'; -import { Component, Input, OnChanges } from '@angular/core'; import { - faCheckCircle, + ChannelItem, + ChannelService, + JobStatus, + JobStatusService, + RevisionItem, +} from 'src/app/core/api/v1'; +import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { + faCircle, faNetworkWired, faTimesCircle, } from '@fortawesome/free-solid-svg-icons'; @@ -16,29 +22,67 @@ import en from 'javascript-time-ago/locale/en'; templateUrl: './overview.component.html', styleUrls: ['./overview.component.css'], }) -export class OverviewComponent implements OnChanges { +export class OverviewComponent implements OnChanges, OnInit, OnDestroy { @Input() channelId = ''; channel!: ChannelItem; + channelStatus!: JobStatus; activeRevision!: RevisionItem | undefined; publishedAt: string | null | undefined; - icons = { faCheckCircle, faTimesCircle, faNetworkWired }; + icons = { faCircle, faTimesCircle, faNetworkWired }; types = ComponentTypes; protocol = window.location.protocol; loading = false; timeAgo: any; + interval: any = null; + timeInterval = 5000; + constructor( private readonly channelService: ChannelService, + private readonly jobStatusService: JobStatusService, private router: Router ) { TimeAgo.addDefaultLocale(en); this.timeAgo = new TimeAgo('en-US'); } + ngOnInit(): void { + this.getJobStatus(); + + this.interval = setInterval(() => { + this.getJobStatus(); + }, this.timeInterval); + } + ngOnChanges(): void { this.refreshData(); } + ngOnDestroy(): void { + clearInterval(this.interval); + } + + getJobStatus(): void { + this.jobStatusService + .apiJobstatusChannelIdGet(this.channelId) + .subscribe((res) => (this.channelStatus = res.status)); + } + + getStatusColor(status: JobStatus | undefined) { + switch (status) { + case JobStatus.Unknown: + return 'gray'; + case JobStatus.Pending: + return 'yellow'; + case JobStatus.Running: + return 'green'; + case JobStatus.Dead: + return 'red'; + default: + return 'gray'; + } + } + refreshData() { this.loading = true; this.channelService.apiChannelIdGet(this.channelId).subscribe({ diff --git a/src/Web/ClientApp/src/app/core/api/v1/api/jobStatus.service.ts b/src/Web/ClientApp/src/app/core/api/v1/api/jobStatus.service.ts index 78f4e495e..aa99ed4e7 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/api/jobStatus.service.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/api/jobStatus.service.ts @@ -18,6 +18,8 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; +// @ts-ignore +import { ChannelJobStatusItem } from '../model/channelJobStatusItem'; // @ts-ignore import { ChannelJobStatusItemPage } from '../model/channelJobStatusItemPage'; @@ -86,6 +88,71 @@ export class JobStatusService { return httpParams; } + /** + * @param channelId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public apiJobstatusChannelIdGet(channelId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext}): Observable; + public apiJobstatusChannelIdGet(channelId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext}): Observable>; + public apiJobstatusChannelIdGet(channelId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext}): Observable>; + public apiJobstatusChannelIdGet(channelId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain' | 'application/json' | 'text/json', context?: HttpContext}): Observable { + if (channelId === null || channelId === undefined) { + throw new Error('Required parameter channelId was null or undefined when calling apiJobstatusChannelIdGet.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (Bearer) required + localVarCredential = this.configuration.lookupCredential('Bearer'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'text/plain', + 'application/json', + 'text/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + return this.httpClient.get(`${this.configuration.basePath}/api/jobstatus/${encodeURIComponent(String(channelId))}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * @param pageIndex * @param pageSize diff --git a/src/Web/ClientApp/src/app/core/api/v1/model/appItem.ts b/src/Web/ClientApp/src/app/core/api/v1/model/appItem.ts index 31ad38ec3..067114444 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/model/appItem.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/model/appItem.ts @@ -13,8 +13,8 @@ import { AppChannelListItem } from './appChannelListItem'; export interface AppItem { - name: string; id: string; + name: string; storageId: string; description?: string | null; channels: Array; diff --git a/src/Web/ClientApp/src/app/core/api/v1/model/createEnvironmentVariableCommand.ts b/src/Web/ClientApp/src/app/core/api/v1/model/createEnvironmentVariableCommand.ts index 011ad79be..eeb727ea3 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/model/createEnvironmentVariableCommand.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/model/createEnvironmentVariableCommand.ts @@ -3,15 +3,17 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ -export interface CreateEnvironmentVariableCommand { + +export interface CreateEnvironmentVariableCommand { key: string; value: string; channelId: string; } + diff --git a/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariableDto.ts b/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariableDto.ts index 8a19d830b..ef040f9d5 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariableDto.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariableDto.ts @@ -3,16 +3,18 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ -export interface EnvironmentVariableDto { + +export interface EnvironmentVariableDto { id: string; channelId: string; key: string; value: string; } + diff --git a/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariablesVm.ts b/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariablesVm.ts index dec0b2404..14b329da7 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariablesVm.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/model/environmentVariablesVm.ts @@ -3,7 +3,7 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -11,6 +11,8 @@ */ import { EnvironmentVariableDto } from './environmentVariableDto'; -export interface EnvironmentVariablesVm { + +export interface EnvironmentVariablesVm { environmentVariables: Array; } + diff --git a/src/Web/ClientApp/src/app/core/api/v1/model/updateEnvironmentVariableCommand.ts b/src/Web/ClientApp/src/app/core/api/v1/model/updateEnvironmentVariableCommand.ts index f26215504..edbd5626a 100644 --- a/src/Web/ClientApp/src/app/core/api/v1/model/updateEnvironmentVariableCommand.ts +++ b/src/Web/ClientApp/src/app/core/api/v1/model/updateEnvironmentVariableCommand.ts @@ -3,15 +3,17 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ -export interface UpdateEnvironmentVariableCommand { + +export interface UpdateEnvironmentVariableCommand { id: string; key: string; value: string; } + diff --git a/tests/Hippo.FunctionalTests/TestBase.cs b/tests/Hippo.FunctionalTests/TestBase.cs index 5d287910e..71b80103e 100644 --- a/tests/Hippo.FunctionalTests/TestBase.cs +++ b/tests/Hippo.FunctionalTests/TestBase.cs @@ -220,6 +220,11 @@ public string[] GetJobLogs(string jobName) return null; } + public Job? GetJob(string jobName) + { + return null; + } + private class NullJob : Job { public void Reload() { }