Merge "dashboard: make ZuulService load info api to manage paths"

This commit is contained in:
Zuul 2018-07-17 16:57:40 +00:00 committed by Gerrit Code Review
commit 20589fffea
16 changed files with 187 additions and 91 deletions

View File

@ -61,6 +61,10 @@ const appRoutes: Routes = [
path: 'tenants.html',
component: TenantsComponent
},
{
path: 't/tenants.html',
component: TenantsComponent
},
{
path: '**',
component: StatusComponent

View File

@ -22,6 +22,8 @@ import { BrowserModule } from '@angular/platform-browser'
import { HttpClientModule } from '@angular/common/http'
import { FormsModule } from '@angular/forms'
import { CoreModule } from './core/core.module'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { getAppBaseHref } from './zuul/zuul.service'
@ -40,6 +42,7 @@ import ZuulService from './zuul/zuul.service'
BrowserModule,
HttpClientModule,
FormsModule,
CoreModule.forRoot({}),
AppRoutingModule,
],
declarations: [

View File

@ -30,16 +30,14 @@ export default class BuildsComponent implements OnInit {
pipeline: string
job_name: string
project: string
tenant: string
constructor(
private http: HttpClient, private route: ActivatedRoute,
private zuul: ZuulService
) {}
ngOnInit() {
this.tenant = this.route.snapshot.paramMap.get('tenant')
async ngOnInit() {
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
this.pipeline = this.route.snapshot.queryParamMap.get('pipeline')
this.job_name = this.route.snapshot.queryParamMap.get('job_name')
@ -54,18 +52,20 @@ export default class BuildsComponent implements OnInit {
if (this.job_name) { params = params.set('job_name', this.job_name) }
if (this.project) { params = params.set('project', this.project) }
const remoteLocation = this.zuul.getSourceUrl('builds', this.tenant)
this.http.get<Build[]>(remoteLocation, {params: params})
.subscribe(builds => {
for (const build of builds) {
/* Fix incorect url for post_failure job */
/* TODO(mordred) Maybe let's fix this server side? */
if (build.log_url === build.job_name) {
build.log_url = undefined
const remoteLocation = this.zuul.getSourceUrl('builds')
if (remoteLocation) {
this.http.get<Build[]>(remoteLocation, {params: params})
.subscribe(builds => {
for (const build of builds) {
/* Fix incorect url for post_failure job */
/* TODO(mordred) Maybe let's fix this server side? */
if (build.log_url === build.job_name) {
build.log_url = undefined
}
}
}
this.builds = builds
})
this.builds = builds
})
}
}
getRowClass(build: Build): string {

View File

@ -39,6 +39,11 @@ module.exports = {
// Each of the entries below lists a specific 'chunk' which is one of the
// entry items from above. We can collapse this to just do one single
// output file.
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'web/config/main.ejs',
title: 'Zuul Status'
}),
new HtmlWebpackPlugin({
filename: 'status.html',
template: 'web/config/main.ejs',

24
web/core/core.module.ts Normal file
View File

@ -0,0 +1,24 @@
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ZuulService } from '../zuul/zuul.service'
@NgModule({
imports: [ CommonModule ],
providers: [ ZuulService ]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only')
}
}
static forRoot(config: {}): ModuleWithProviders {
return {
ngModule: CoreModule,
}
}
}

View File

@ -26,21 +26,23 @@ import Job from './job'
export default class JobsComponent implements OnInit {
jobs: Job[]
tenant?: string
constructor(
private http: HttpClient, private route: ActivatedRoute,
private zuul: ZuulService
) {}
ngOnInit() {
this.tenant = this.route.snapshot.paramMap.get('tenant')
async ngOnInit() {
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
this.jobsFetch()
}
jobsFetch(): void {
this.http.get<Job[]>(this.zuul.getSourceUrl('jobs', this.tenant))
.subscribe(jobs => this.injestJobs(jobs))
const remoteLocation = this.zuul.getSourceUrl('jobs')
if (remoteLocation) {
this.http.get<Job[]>(remoteLocation)
.subscribe(jobs => this.injestJobs(jobs))
}
}
injestJobs(jobs: Job[]): void {

View File

@ -16,10 +16,11 @@ under the License.
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" [routerLink]="zuul.appBaseHref" target="_self">Zuul Dashboard</a>
<a *ngIf="zuul.info && !zuul.info.whiteLabel" class="navbar-brand" [routerLink]="dashboardLink" target="_self">Zuul Dashboard</a>
<span *ngIf="zuul.info && zuul.info.whiteLabel" class="navbar-brand">Zuul Dashboard</span>
</div>
<ul class="nav navbar-nav" *ngIf="showNavbar">
<li routerLinkActive="active" *ngFor="let route of navbarRoutes">
<ul class="nav navbar-nav" *ngIf="zuul.info && zuul.info.tenant !== ''">
<li routerLinkActive="active" *ngFor="let route of zuul.navbarRoutes">
<a [routerLink]="route.url">
{{ route.title }}
</a>

View File

@ -19,57 +19,16 @@ import { filter } from 'rxjs/operators'
import ZuulService from '../zuul/zuul.service'
import RouteDescription from './description'
@Component({
selector: 'navigation',
template: require('./navigation.component.html')
})
export default class NavigationComponent implements OnInit {
navbarRoutes: RouteDescription[]
resolveEnd$: Observable<ResolveEnd>
showNavbar = true
private routePages = ['status', 'jobs', 'builds']
dashboardLink: string
constructor(private router: Router, private zuul: ZuulService) {}
ngOnInit() {
this.resolveEnd$ = this.router.events.pipe(
filter(evt => evt instanceof ResolveEnd)
) as Observable<ResolveEnd>
this.resolveEnd$.subscribe(evt => {
this.showNavbar = (evt.url !== '/tenants.html')
this.navbarRoutes = this.getNavbarRoutes(evt.url)
})
}
getNavbarRoutes(url: string): RouteDescription[] {
const routes = []
for (const routePage of this.routePages) {
const description: RouteDescription = {
title: this.getRouteTitle(routePage),
url: this.getRouterLink(routePage, url)
}
routes.push(description)
}
return routes
}
getRouteTitle(target: string): string {
return target.charAt(0).toUpperCase() + target.slice(1)
}
getRouterLink(target: string, url: string): string[] {
const htmlTarget = target + '.html'
// NOTE: This won't work if there is a tenant name with a / in it. It would
// be great to pull the tenant name from the paramMap- but so far I can't
// find a way to get to the current paramMap of the current component from
// inside of this component.
if (url.startsWith('/t/')) {
return ['/t', url.split('/')[2], htmlTarget]
} else {
return ['/' + htmlTarget]
}
async ngOnInit() {
this.dashboardLink = '/t/tenants.html'
}
}

View File

@ -36,10 +36,12 @@ export default class StatusComponent implements OnInit, OnDestroy {
constructor(private route: ActivatedRoute, private zuul: ZuulService) {}
ngOnInit() {
async ngOnInit() {
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
if (typeof this.app === 'undefined') {
this.app = zuulStart(
jQuery, this.route.snapshot.paramMap.get('tenant'), this.zuul)
jQuery, this.zuul)
}
this.app.options.enabled = true
}

View File

@ -26,7 +26,7 @@ import './jquery.zuul'
/**
* @return The $.zuul instance
*/
function zuulStart ($, tenant, zuulService) {
function zuulStart ($, zuulService) {
// Start the zuul app (expects default dom)
let $container, $indicator
@ -50,7 +50,7 @@ function zuulStart ($, tenant, zuulService) {
params['source_data'] = DemoStatusTree
}
} else {
params['source'] = zuulService.getSourceUrl('status', tenant)
params['source'] = zuulService.getSourceUrl('status')
}
let zuul = $.zuul(params, zuulService)

View File

@ -34,7 +34,8 @@ export default class StreamComponent implements OnInit {
constructor(private route: ActivatedRoute, private zuul: ZuulService) {}
ngOnInit() {
async ngOnInit() {
await this.zuul.setTenant(this.route.snapshot.paramMap.get('tenant'))
this.startStream()
}
@ -54,7 +55,6 @@ export default class StreamComponent implements OnInit {
}, pageUpdateInMS)
const queryParamMap = this.route.snapshot.queryParamMap
const tenant = this.route.snapshot.paramMap.get('tenant')
const params = {
uuid: queryParamMap.get('uuid')
@ -68,8 +68,7 @@ export default class StreamComponent implements OnInit {
} else if (queryParamMap.has('websocket_url')) {
params['websocket_url'] = queryParamMap.get('websocket_url')
} else {
params['websocket_url'] = this.zuul.getWebsocketUrl(
'console-stream', tenant)
params['websocket_url'] = this.zuul.getWebsocketUrl('console-stream')
}
const ws = new WebSocket(params['websocket_url'])

View File

@ -26,7 +26,8 @@ export default class TenantsComponent implements OnInit {
constructor(private http: HttpClient, private zuul: ZuulService) {}
ngOnInit() {
async ngOnInit() {
await this.zuul.setTenant()
this.tenantsFetch()
}

18
web/zuul/info.ts Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2018 Red Hat
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
export default class Info {
tenant: string
whiteLabel: boolean
}

19
web/zuul/infoResponse.ts Normal file
View File

@ -0,0 +1,19 @@
// Copyright 2018 Red Hat
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
import Info from './info'
export default class InfoResponse {
info: Info
}

View File

@ -12,11 +12,17 @@
// License for the specific language governing permissions and limitations
// under the License.
import { Injectable } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { Injectable, EventEmitter, Output } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Router } from '@angular/router'
import { BehaviorSubject } from 'rxjs/BehaviorSubject'
import * as url from 'url'
import Info from './info'
import InfoResponse from './infoResponse'
import RouteDescription from './description'
declare var ZUUL_API_URL: string
declare var ZUUL_BASE_HREF: string
@ -48,17 +54,39 @@ export function getAppBaseHref (): string {
return path
}
@Injectable()
class ZuulService {
@Injectable({
providedIn: 'root',
})
export class ZuulService {
public baseApiUrl: string
public appBaseHref: string
public info: Info
navbarRoutes: RouteDescription[]
private routePages = ['status', 'jobs', 'builds']
constructor() {
constructor(private router: Router, private http: HttpClient) {
this.baseApiUrl = this.getBaseApiUrl()
this.appBaseHref = getAppBaseHref()
}
async setTenant (tenant?: string) {
if (!this.info) {
const infoEndpoint = this.baseApiUrl + 'api/info'
const infoResponse = await this.http.get<InfoResponse>(
infoEndpoint).toPromise()
this.info = infoResponse.info
if (this.info.tenant && !tenant) {
this.info.whiteLabel = true
} else {
this.info.whiteLabel = false
}
}
if (tenant) {
this.info.tenant = tenant
}
this.navbarRoutes = this.getNavbarRoutes()
}
getBaseApiUrl (): string {
let path
if (typeof ZUUL_API_URL !== 'undefined') {
@ -72,22 +100,53 @@ class ZuulService {
return path
}
getSourceUrl (filename: string, tenant?: string): string {
if (tenant) {
// Multi-tenant deploy. This is at t/a-tenant/x.html
return url.resolve(this.baseApiUrl, `api/tenant/${tenant}/${filename}`)
} else {
// Whitelabel deploy or tenants list, such as /status.html,
// /tenants.html or /zuul/status.html or /zuul/tenants.html
getSourceUrl (filename: string): string {
const tenant = this.info.tenant
if (this.info.whiteLabel || filename === 'tenants') {
if (!this.info.whiteLabel) {
// Reset selected tenant
this.info.tenant = ''
}
return url.resolve(this.baseApiUrl, `api/${filename}`)
}
if (!tenant) {
// No tenant selected, go to tenant list
console.log('No tenant selected, navigate to tenants list')
this.router.navigate(['/tenants.html'])
}
return url.resolve(this.baseApiUrl, `api/tenant/${tenant}/${filename}`)
}
getWebsocketUrl (filename: string, tenant?: string): string {
return this.getSourceUrl(filename, tenant)
getWebsocketUrl (filename: string): string {
return this.getSourceUrl(filename)
.replace(/(http)(s)?\:\/\//, 'ws$2://')
}
getNavbarRoutes(): RouteDescription[] {
const routes = []
for (const routePage of this.routePages) {
const description: RouteDescription = {
title: this.getRouteTitle(routePage),
url: this.getRouterLink(routePage)
}
routes.push(description)
}
return routes
}
getRouteTitle(target: string): string {
return target.charAt(0).toUpperCase() + target.slice(1)
}
getRouterLink(target: string): string[] {
const htmlTarget = target + '.html'
if (this.info.whiteLabel) {
return ['/' + htmlTarget]
} else {
return ['/t', this.info.tenant, htmlTarget]
}
}
}
export default ZuulService