Skip to content

Commit 43d502c

Browse files
authored
automate infra proxy cookbook list UI (#3014)
* added cookbook list module Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * added some minor fixes Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * added minor UI fixes Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * table alignment fix added Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * fixed side nav issues Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * test specs fixed Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * added three dot fixed Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * added minor lable changes Signed-off-by: Sachin Bachhav <sbachhav@chef.io> * added file in exception to skip cred scan Signed-off-by: Sachin Bachhav <sbachhav@chef.io>
1 parent 795d717 commit 43d502c

23 files changed

+640
-30
lines changed

components/automate-ui/src/app/app-routing.module.ts

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { SigninComponent } from './pages/signin/signin.component';
2727
import { AutomateSettingsComponent } from './pages/automate-settings/automate-settings.component';
2828
import { ChefServersListComponent } from './modules/infra-proxy/chef-servers-list/chef-servers-list.component';
2929
import { ChefServerDetailsComponent } from './modules/infra-proxy/chef-server-details/chef-server-details.component';
30+
import { CookbooksListComponent } from './modules/infra-proxy/cookbook-list/cookbooks-list.component';
3031
import { NodeDetailsComponent } from './pages/node-details/node-details.component';
3132
import {
3233
NodeNoRunsDetailsComponent
@@ -243,6 +244,10 @@ const routes: Routes = [
243244
{
244245
path: ':id',
245246
component: ChefServerDetailsComponent
247+
},
248+
{
249+
path: ':id/org/:orgid/cookbooks',
250+
component: CookbooksListComponent
246251
}
247252
]
248253
}

components/automate-ui/src/app/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { TelemetryService } from './services/telemetry/telemetry.service';
6868
// Requests
6969
import { ApiTokenRequests } from './entities/api-tokens/api-token.requests';
7070
import { AutomateSettingsRequests } from './entities/automate-settings/automate-settings.requests';
71+
import { CookbookRequests } from './entities/cookbooks/cookbook.requests';
7172
import { ClientRunsRequests } from './entities/client-runs/client-runs.requests';
7273
import { CredentialRequests } from './entities/credentials/credential.requests';
7374
import { JobRequests } from './entities/jobs/job.requests';
@@ -291,6 +292,7 @@ import { WelcomeModalComponent } from './page-components/welcome-modal/welcome-m
291292
ChefSessionService,
292293
ConfigService,
293294
ClientRunsRequests,
295+
CookbookRequests,
294296
CredentialRequests,
295297
DatafeedService,
296298
EventFeedService,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { HttpErrorResponse } from '@angular/common/http';
2+
import { Action } from '@ngrx/store';
3+
4+
import { Cookbook } from './cookbook.model';
5+
6+
export enum CookbookActionTypes {
7+
GET_ALL = 'COOKBOOKS::GET_ALL',
8+
GET_ALL_SUCCESS = 'COOKBOOKS::GET_ALL::SUCCESS',
9+
GET_ALL_FAILURE = 'COOKBOOKS::GET_ALL::FAILURE'
10+
}
11+
12+
export interface CookbookSuccessPayload {
13+
cookbook: Cookbook;
14+
}
15+
16+
export class GetCookbooksForOrg implements Action {
17+
readonly type = CookbookActionTypes.GET_ALL;
18+
19+
constructor(public payload: { server_id: string, org_id: string }) { }
20+
}
21+
22+
export interface CookbooksSuccessPayload {
23+
cookbooks: Cookbook[];
24+
}
25+
26+
export class GetCookbooksSuccess implements Action {
27+
readonly type = CookbookActionTypes.GET_ALL_SUCCESS;
28+
29+
constructor(public payload: CookbooksSuccessPayload) { }
30+
}
31+
32+
export class GetCookbooksFailure implements Action {
33+
readonly type = CookbookActionTypes.GET_ALL_FAILURE;
34+
35+
constructor(public payload: HttpErrorResponse) { }
36+
}
37+
38+
export type CookbookActions =
39+
| GetCookbooksForOrg
40+
| GetCookbooksSuccess
41+
| GetCookbooksFailure;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpErrorResponse } from '@angular/common/http';
3+
import { Actions, Effect, ofType } from '@ngrx/effects';
4+
import { of as observableOf } from 'rxjs';
5+
import { catchError, mergeMap, map } from 'rxjs/operators';
6+
7+
import { CreateNotification } from 'app/entities/notifications/notification.actions';
8+
import { Type } from 'app/entities/notifications/notification.model';
9+
10+
import {
11+
GetCookbooksForOrg,
12+
GetCookbooksSuccess,
13+
CookbooksSuccessPayload,
14+
GetCookbooksFailure,
15+
CookbookActionTypes
16+
} from './cookbook.actions';
17+
18+
import {
19+
CookbookRequests
20+
} from './cookbook.requests';
21+
22+
@Injectable()
23+
export class CookbookEffects {
24+
constructor(
25+
private actions$: Actions,
26+
private requests: CookbookRequests
27+
) { }
28+
29+
@Effect()
30+
getCookbooksForOrgs$ = this.actions$.pipe(
31+
ofType(CookbookActionTypes.GET_ALL),
32+
mergeMap(({ payload: { server_id, org_id } }: GetCookbooksForOrg) =>
33+
this.requests.getCookbooksForOrgs(server_id, org_id).pipe(
34+
map((resp: CookbooksSuccessPayload) => new GetCookbooksSuccess(resp)),
35+
catchError((error: HttpErrorResponse) => observableOf(new GetCookbooksFailure(error))))));
36+
37+
@Effect()
38+
getCookbooksFailure$ = this.actions$.pipe(
39+
ofType(CookbookActionTypes.GET_ALL_FAILURE),
40+
map(({ payload }: GetCookbooksFailure) => {
41+
const msg = payload.error.error;
42+
return new CreateNotification({
43+
type: Type.error,
44+
message: `Could not get cookbooks: ${msg || payload.error}`
45+
});
46+
}));
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Cookbook {
2+
name: string;
3+
version: string;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
2+
import { set, pipe } from 'lodash/fp';
3+
4+
import { EntityStatus } from 'app/entities/entities';
5+
import { CookbookActionTypes, CookbookActions } from './cookbook.actions';
6+
import { Cookbook } from './cookbook.model';
7+
8+
export interface CookbookEntityState extends EntityState<Cookbook> {
9+
getAllStatus: EntityStatus;
10+
getStatus: EntityStatus;
11+
}
12+
13+
const GET_ALL_STATUS = 'getAllStatus';
14+
15+
export const cookbookEntityAdapter: EntityAdapter<Cookbook> = createEntityAdapter<Cookbook>({
16+
selectId: (cookbook: Cookbook) => cookbook.name
17+
});
18+
19+
export const CookbookEntityInitialState: CookbookEntityState =
20+
cookbookEntityAdapter.getInitialState(<CookbookEntityState>{
21+
getAllStatus: EntityStatus.notLoaded,
22+
getStatus: EntityStatus.notLoaded
23+
});
24+
25+
export function cookbookEntityReducer(
26+
state: CookbookEntityState = CookbookEntityInitialState,
27+
action: CookbookActions): CookbookEntityState {
28+
29+
switch (action.type) {
30+
case CookbookActionTypes.GET_ALL:
31+
return set(GET_ALL_STATUS, EntityStatus.loading, cookbookEntityAdapter.removeAll(state));
32+
33+
case CookbookActionTypes.GET_ALL_SUCCESS:
34+
return pipe(
35+
set(GET_ALL_STATUS, EntityStatus.loadingSuccess))
36+
(cookbookEntityAdapter.addAll(action.payload.cookbooks, state)) as CookbookEntityState;
37+
38+
case CookbookActionTypes.GET_ALL_FAILURE:
39+
return set(GET_ALL_STATUS, EntityStatus.loadingFailure, state);
40+
41+
default:
42+
return state;
43+
}
44+
}
45+
46+
export const getEntityById = (name: string) => (state: CookbookEntityState) => state.entities[name];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { Observable } from 'rxjs';
4+
import { environment as env } from 'environments/environment';
5+
6+
import {
7+
CookbooksSuccessPayload
8+
} from './cookbook.actions';
9+
10+
@Injectable()
11+
export class CookbookRequests {
12+
13+
constructor(private http: HttpClient) { }
14+
15+
// tslint:disable-next-line: max-line-length
16+
public getCookbooksForOrgs(server_id: string, org_id: string): Observable<CookbooksSuccessPayload> {
17+
return this.http.get<CookbooksSuccessPayload>(
18+
`${env.infra_proxy_url}/servers/${server_id}/orgs/${org_id}/cookbooks`);
19+
}
20+
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createSelector, createFeatureSelector } from '@ngrx/store';
2+
import { CookbookEntityState, cookbookEntityAdapter } from './cookbook.reducer';
3+
4+
export const cookbookState = createFeatureSelector<CookbookEntityState>('cookbooks');
5+
6+
export const {
7+
selectAll: allCookbooks,
8+
selectEntities: cookbookEntities
9+
} = cookbookEntityAdapter.getSelectors(cookbookState);
10+
11+
export const getAllStatus = createSelector(
12+
cookbookState,
13+
(state) => state.getAllStatus
14+
);
15+
16+
export const getStatus = createSelector(
17+
cookbookState,
18+
(state) => state.getStatus
19+
);

components/automate-ui/src/app/entities/layout/layout-sidebar.service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export class LayoutSidebarService implements OnInit, OnDestroy {
7676
name: 'Chef Servers',
7777
icon: 'storage',
7878
route: '/infrastructure/chef-servers',
79+
authorized: {
80+
anyOf: [['/infra/servers', 'get']]
81+
},
7982
visible$: new BehaviorSubject(this.chefInfraServerViewsFeatureFlagOn)
8083
},
8184
{

components/automate-ui/src/app/entities/orgs/org.selectors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ export const deleteStatus = createSelector(
4343
export const orgFromRoute = createSelector(
4444
orgEntities,
4545
routeParams,
46-
(state, { orgId }) => state[orgId]
46+
(state, { orgid }) => state[orgid]
4747
);

components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.html

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="container">
33
<main>
44
<chef-breadcrumbs>
5-
<chef-breadcrumb [link]="['/infrastructure/chef-servers']">Chef Server</chef-breadcrumb>
5+
<chef-breadcrumb [link]="['/infrastructure/chef-servers']">Chef Servers</chef-breadcrumb>
66
{{ server?.name }}
77
</chef-breadcrumbs>
88
<chef-page-header>
@@ -46,7 +46,7 @@
4646
<form [formGroup]="updateServerForm">
4747
<chef-form-field id="update-name">
4848
<label>
49-
<span class="label">Chef Server Name <span aria-hidden="true">*</span></span>
49+
<span class="label">Name <span aria-hidden="true">*</span></span>
5050
<input chefInput formControlName="name" type="text" autocomplete="off"
5151
data-cy="update-chefServer-name">
5252
</label>
@@ -101,7 +101,7 @@
101101
<section class="page-body" *ngIf="tabValue === 'orgs'">
102102
<ng-container>
103103
<chef-toolbar>
104-
<chef-button id="create-button" primary (click)="openCreateModal('create')">Create Org</chef-button>
104+
<chef-button id="create-button" primary (click)="openCreateModal('create')">Add Org</chef-button>
105105
</chef-toolbar>
106106
<chef-table-new>
107107
<chef-table-header>
@@ -114,11 +114,11 @@
114114
<chef-table-body>
115115
<chef-table-row *ngFor="let org of orgs">
116116
<chef-table-cell>
117-
<a [routerLink]="['/infrastructure','chef-servers', server?.id, 'orgs', org.id, 'cookbooks']">{{ org.name }}</a>
117+
<a [routerLink]="['/infrastructure','chef-servers', server?.id, 'org', org.id, 'cookbooks']">{{ org.name }}</a>
118118
</chef-table-cell>
119119
<chef-table-cell>{{ org.admin_user }}</chef-table-cell>
120120
<chef-table-cell class="three-dot-column">
121-
<mat-select>
121+
<mat-select panelClass="chef-control-menu" id="menu-{{org.id}}">
122122
<mat-option data-cy="delete" (onSelectionChange)="startOrgDelete($event, org)">Delete Org</mat-option>
123123
</mat-select>
124124
</chef-table-cell>

components/automate-ui/src/app/modules/infra-proxy/chef-server-details/chef-server-details.component.scss

-14
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,3 @@ chef-table-new {
111111
}
112112
}
113113

114-
.empty-case-container {
115-
margin-top: 42px;
116-
justify-content: center;
117-
text-align: center;
118-
119-
.empty-case-entry {
120-
margin-top: 18px;
121-
margin-bottom: 0;
122-
}
123-
124-
p {
125-
font-size: 18px;
126-
}
127-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<div class="content-container">
2+
<div class="container">
3+
<main>
4+
<chef-breadcrumbs>
5+
<chef-breadcrumb [link]="['/infrastructure/chef-servers']">Chef Servers</chef-breadcrumb>
6+
<chef-breadcrumb [routerLink]="['/infrastructure/chef-servers', org?.server_id]">Orgs</chef-breadcrumb>
7+
{{ org?.name }}
8+
</chef-breadcrumbs>
9+
<chef-page-header>
10+
<chef-heading>{{ org?.name }}</chef-heading>
11+
<table>
12+
<thead>
13+
<tr class="detail-row">
14+
<th class="id-column">Name</th>
15+
<th class="id-column">Admin User</th>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
<tr class="detail-row">
20+
<td class="id-column">{{ org?.name }}</td>
21+
<td class="id-column">{{ org?.admin_user }}</td>
22+
</tr>
23+
</tbody>
24+
</table>
25+
<chef-tab-selector [value]="tabValue" (change)="onSelectedTab($event)">
26+
<chef-option value='cookbooks' data-cy="cookbooks-tab">Cookbooks</chef-option>
27+
<chef-option value='details' data-cy="details-tab">Details</chef-option>
28+
</chef-tab-selector>
29+
</chef-page-header>
30+
<section class="page-body" *ngIf="tabValue === 'details'">
31+
<form [formGroup]="updateOrgForm">
32+
<chef-form-field>
33+
<label>
34+
<span class="label">Name <span aria-hidden="true">*</span></span>
35+
<input chefInput formControlName="name" type="text" autocomplete="off"
36+
data-cy="update-org-name">
37+
</label>
38+
<chef-error
39+
*ngIf="(updateOrgForm.get('name').hasError('required') || updateOrgForm.get('name').hasError('pattern')) && updateOrgForm.get('name').dirty">
40+
Display Name is required.
41+
</chef-error>
42+
</chef-form-field>
43+
<chef-form-field>
44+
<label>
45+
<span class="label">Admin User <span aria-hidden="true">*</span></span>
46+
<input chefInput formControlName="admin_user" type="text" autocomplete="off"
47+
data-cy="update-Org-admin-user">
48+
</label>
49+
<chef-error
50+
*ngIf="(updateOrgForm.get('admin_user').hasError('required') || updateOrgForm.get('admin_user').hasError('pattern')) && updateOrgForm.get('admin_user').dirty">
51+
Admin User is required.
52+
</chef-error>
53+
</chef-form-field>
54+
<chef-form-field>
55+
<label>
56+
<span class="label">Admin Key<span aria-hidden="true">*</span></span>
57+
<textarea rows="27" cols="10" chefInput placeholder="-----BEGIN RSA PRIVATE KEY -----"
58+
formControlName="admin_key" data-cy="update-org-admin-key"></textarea>
59+
</label>
60+
<chef-error
61+
*ngIf="(updateOrgForm.get('admin_key').hasError('required') || updateOrgForm.get('admin_key').hasError('pattern')) && updateOrgForm.get('admin_key').dirty">
62+
Admin Key is required.
63+
</chef-error>
64+
</chef-form-field>
65+
<chef-form-field>
66+
<div id="button-bar">
67+
<chef-button [disabled]="isLoading || !updateOrgForm.valid || !updateOrgForm.dirty" primary
68+
inline (click)="saveOrg()">
69+
<chef-loading-spinner *ngIf="saving"></chef-loading-spinner>
70+
<span *ngIf="saving">Saving...</span>
71+
<span *ngIf="!saving">Save</span>
72+
</chef-button>
73+
<span id="saved-note" *ngIf="saveSuccessful && !updateOrgForm.dirty">All changes
74+
saved.</span>
75+
</div>
76+
</chef-form-field>
77+
</form>
78+
</section>
79+
<section class="page-body" *ngIf="tabValue === 'cookbooks'">
80+
<ng-container>
81+
<chef-table-new>
82+
<chef-table-header>
83+
<chef-table-row>
84+
<chef-table-header-cell>Cookbook Name</chef-table-header-cell>
85+
<chef-table-header-cell>Cookbook Version</chef-table-header-cell>
86+
</chef-table-row>
87+
</chef-table-header>
88+
<chef-table-body>
89+
<chef-table-row *ngFor="let cookbook of cookbooks">
90+
<chef-table-cell>{{ cookbook.name }}</chef-table-cell>
91+
<chef-table-cell>{{ cookbook.version }}</chef-table-cell>
92+
</chef-table-row>
93+
</chef-table-body>
94+
</chef-table-new>
95+
</ng-container>
96+
</section>
97+
</main>
98+
</div>
99+
</div>

0 commit comments

Comments
 (0)