commit 43c141b5a83b41babe057ffe29dfc315f0e206d1 Author: Matthew Fuller Date: Fri Oct 9 21:41:24 2020 +0000 Refactor Phase and Document components Moves all phase related functionality to the Phase component, and document functionality (currently just doc pull) to the Document component. Also refactors backend code to ensure phase and document messages are handled by separate handlers. Change-Id: I64f4f4edeea58608f475e008594ac8dbdea2a5d7 diff --git a/client/src/app/ctl/ctl.module.ts b/client/src/app/ctl/ctl.module.ts index 0900fac..28dcd6e 100644 --- a/client/src/app/ctl/ctl.module.ts +++ b/client/src/app/ctl/ctl.module.ts @@ -18,6 +18,7 @@ import { CtlComponent } from './ctl.component'; import { DocumentModule } from './document/document.module'; import { BaremetalModule } from './baremetal/baremetal.module'; import { CtlRoutingModule } from './ctl-routing.module'; +import { PhaseModule } from './phase/phase.module'; @NgModule({ imports: [ @@ -26,6 +27,7 @@ import { CtlRoutingModule } from './ctl-routing.module'; RouterModule, DocumentModule, BaremetalModule, + PhaseModule ], declarations: [CtlComponent], providers: [] diff --git a/client/src/app/ctl/document/document-viewer/document-viewer.component.css b/client/src/app/ctl/document/document-viewer/document-viewer.component.css deleted file mode 100644 index a650336..0000000 --- a/client/src/app/ctl/document/document-viewer/document-viewer.component.css +++ /dev/null @@ -1,69 +0,0 @@ -/* -# 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. -*/ - -.viewer-editor { - height: inherit; - width: inherit; -} - -ngx-monaco-editor { - width: 100%; - height: 100%; -} - -.filter-param { - height: 25px; - padding-left: 5px; -} - -h4 { - color: blue; -} - -.result-items { - height: 100%; - width: 40%; - top: 48px; - overflow: auto; -} - -.rendered-container { - display: flex; - flex-direction: row; - width: 100%; - height: 78vh; -} - -.editor-div { - width: 60%; - height: 100%; -} - -fieldset { - border: 0; -} - -.spacer { - flex: 1 1 auto; -} - -.apply-div { - display: flex; - flex-direction: row; -} - -.spinner { - margin-left: 15px; - margin-top: 5px; -} \ No newline at end of file diff --git a/client/src/app/ctl/document/document-viewer/document-viewer.component.html b/client/src/app/ctl/document/document-viewer/document-viewer.component.html deleted file mode 100644 index 90b2e93..0000000 --- a/client/src/app/ctl/document/document-viewer/document-viewer.component.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - Phase '{{name}}' - - - - - -
-
-
-

Filter documents by:

-

- - Name - - -

-

- - Namespace - - -

-

- - GVK - - -

-

- - Kind - - -

-

- - Label - - -

-

- - Annotation - - -

-
- - -
-
-
-
-
- -
-
- - - - - - - - -
-
- -
-
-
-
diff --git a/client/src/app/ctl/document/document-viewer/document-viewer.component.ts b/client/src/app/ctl/document/document-viewer/document-viewer.component.ts deleted file mode 100644 index 08a7080..0000000 --- a/client/src/app/ctl/document/document-viewer/document-viewer.component.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* -# 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 {Component, OnInit} from '@angular/core'; -import {MatDialogRef} from '@angular/material/dialog'; -import {KustomNode} from '../document.models'; -import {WebsocketMessage} from '../../../../services/websocket/websocket.models'; -import {WebsocketService} from '../../../../services/websocket/websocket.service'; -import {FormControl, FormGroup} from '@angular/forms'; - -@Component({ - selector: 'app-document-viewer', - templateUrl: 'document-viewer.component.html', - styleUrls: ['./document-viewer.component.css'] - -}) - -export class DocumentViewerComponent implements OnInit { - editorOptions = {language: 'yaml', automaticLayout: true, readOnly: true, theme: 'airshipTheme'}; - bundleYaml: string; - executorYaml: string; - phaseDetails: string; - loading: boolean; - resultsMsg = ''; - - results: KustomNode[] = []; - id: string; - name: string; - yaml: string; - - filterOptions = new FormGroup({ - name: new FormControl(''), - namespace: new FormControl(''), - gvk: new FormControl(''), - kind: new FormControl(''), - label: new FormControl(''), - annotation: new FormControl('') - }); - - constructor( - public dialogRef: MatDialogRef, - private websocketService: WebsocketService) {} - - ngOnInit(): void { - this.bundleYaml = this.yaml; - if (this.bundleYaml !== '') { - this.getDocumentsBySelector('{}'); - } - this.yaml = this.phaseDetails; - } - - onClose(): void { - this.dialogRef.close(); - this.results = null; - } - - setModel(val: string): void { - switch (val) { - case 'bundle': - this.yaml = this.bundleYaml; - break; - case 'executor': - this.yaml = this.executorYaml; - break; - case 'details': - this.yaml = this.phaseDetails; - break; - } - } - - getDocumentsBySelector(selector: string): void { - const msg = new WebsocketMessage('ctl', 'document', 'getDocumentsBySelector'); - msg.message = selector; - msg.id = this.id; - this.websocketService.sendMessage(msg); - } - - getYaml(id: string): void { - this.yaml = null; - const msg = new WebsocketMessage('ctl', 'document', 'getYaml'); - msg.id = id; - msg.message = 'rendered'; - this.websocketService.sendMessage(msg); - } - - onSubmit(data: any): void { - this.loading = true; - this.results = []; - this.resultsMsg = ''; - const selector = {}; - Object.keys(this.filterOptions.controls).forEach(key => { - if (this.filterOptions.controls[key].value !== '') { - if (key === 'gvk') { - const str: string = this.filterOptions.controls[key].value; - const arr = str.split(' '); - selector[key] = { - group: arr[0], - version: arr[1], - kind: arr[2] - }; - } else { - selector[key] = this.filterOptions.controls[key].value; - } - } - }); - this.getDocumentsBySelector(JSON.stringify(selector)); - } -} diff --git a/client/src/app/ctl/document/document-viewer/document-viewer.module.ts b/client/src/app/ctl/document/document-viewer/document-viewer.module.ts deleted file mode 100644 index 340582a..0000000 --- a/client/src/app/ctl/document/document-viewer/document-viewer.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* -# 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 {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatListModule } from '@angular/material/list'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { MatGridListModule } from '@angular/material/grid-list'; -import {MatInputModule} from '@angular/material/input'; -import {MonacoEditorModule} from 'ngx-monaco-editor'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { DocumentViewerComponent } from './document-viewer.component'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltipModule } from '@angular/material/tooltip'; - -@NgModule({ - declarations: [ - DocumentViewerComponent - ], - imports: [ - CommonModule, - MatFormFieldModule, - MatListModule, - MatIconModule, - MatButtonModule, - FormsModule, - ReactiveFormsModule, - MatGridListModule, - MatFormFieldModule, - MatInputModule, - MonacoEditorModule, - MatProgressSpinnerModule, - MatSidenavModule, - MatToolbarModule, - MatTooltipModule - ], - providers: [], -}) -export class DocumentViewerModule {} diff --git a/client/src/app/ctl/document/document.component.css b/client/src/app/ctl/document/document.component.css index 2869f96..76a0870 100644 --- a/client/src/app/ctl/document/document.component.css +++ b/client/src/app/ctl/document/document.component.css @@ -1,120 +1,14 @@ -/* -# 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. -*/ - -.explorer-container { - display: flex; - flex-direction: row; - height: 100%; -} - -/* Phase Tree styles */ - -.grey-icon { - color: grey; - height: 20px; - width: 20px; -} - -.error-icon { - padding-left: 20px; - color: red; - height: 20px; - width: 20px; -} - -.docless-phase { - padding-left: 20px; -} - -.docless-phase-btn { - padding-left: 20px; - padding-right: 0px; -} - -.phase-tree-invisible { - display: none; -} - -.mat-tree-node { - min-height: 25px; -} - -.nested-ul { - padding-left: 0px; -} - -.phase-tree ul, -.phase-tree li { - padding-left: 20px; - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -.phase-tree { - height: 100%; -} - -.phase-tree-card { - height: calc(100vh - 176px); - width: 40%; - box-sizing: border-box; -} - -.phase-card-content { - max-height: 90%; - box-sizing: border-box; - overflow-y: auto; -} - -.phase-tree-progress-bar { - margin-left: 5px; - margin-right: 5px; -} - -.phase-tree-toggle { - padding-right: 0px; -} - -/* Editor styles */ -.editor-btn { - margin-right: 5px; - margin-left: 5px; -} - -.editor-button-grp { - display: flex; -} - -.editor-btns { - margin-left: auto; - margin-right: 30px; -} - -.editor-card { - height: calc(100vh - 176px); - width: 60%; - box-sizing: border-box; -} - -.editor-card-content { - height: 85%; - width: 100%; - margin-bottom: 0px; -} - -ngx-monaco-editor { - height: 100%; - width: 100%; -} +/* +# 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. +*/ + diff --git a/client/src/app/ctl/document/document.component.html b/client/src/app/ctl/document/document.component.html index 778d61b..5e2ee30 100644 --- a/client/src/app/ctl/document/document.component.html +++ b/client/src/app/ctl/document/document.component.html @@ -1,106 +1,4 @@ - -
- - - Phases - Target: {{targetPath}} - - - - - -
  • -
    -
    - -
    -
    - -
    - - - - - - - -
    -
    -
    -
  • -
    - -
  • -
    - - - - - - - -
    -
      - -
    -
  • -
    -
    -
    -
    - - - - {{editorTitle}} - {{editorSubtitle}} - - - - - -
    -
    - - -
    -
    -
    -
    -
    -

    diff --git a/client/src/app/ctl/document/document.component.spec.ts b/client/src/app/ctl/document/document.component.spec.ts index 8d9c12d..bce8304 100644 --- a/client/src/app/ctl/document/document.component.spec.ts +++ b/client/src/app/ctl/document/document.component.spec.ts @@ -15,19 +15,9 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {DocumentComponent} from './document.component'; import {MatTabsModule} from '@angular/material/tabs'; -import {MatTreeModule} from '@angular/material/tree'; import {MatButtonModule} from '@angular/material/button'; -import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {MatIconModule} from '@angular/material/icon'; -import {MonacoEditorModule} from 'ngx-monaco-editor'; -import {FormsModule} from '@angular/forms'; -import {ToastrModule} from 'ngx-toastr'; -import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {MatCardModule} from '@angular/material/card'; -import {MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatMenuModule } from '@angular/material/menu'; +import { ToastrModule } from 'ngx-toastr'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; describe('DocumentComponent', () => { let component: DocumentComponent; @@ -36,20 +26,10 @@ describe('DocumentComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - BrowserAnimationsModule, MatTabsModule, - MatTreeModule, MatButtonModule, - MatButtonToggleModule, - MatIconModule, - MonacoEditorModule, - FormsModule, ToastrModule.forRoot(), - MatCardModule, - MatProgressBarModule, - MatTooltipModule, - MatDialogModule, - MatMenuModule + BrowserAnimationsModule ], declarations: [DocumentComponent] }) diff --git a/client/src/app/ctl/document/document.component.ts b/client/src/app/ctl/document/document.component.ts index 6384ecb..3b23203 100644 --- a/client/src/app/ctl/document/document.component.ts +++ b/client/src/app/ctl/document/document.component.ts @@ -17,12 +17,6 @@ import {WebsocketService} from '../../../services/websocket/websocket.service'; import {WebsocketMessage, WSReceiver} from '../../../services/websocket/websocket.models'; import {Log} from '../../../services/log/log.service'; import {LogMessage} from '../../../services/log/log-message'; -import {KustomNode, RunOptions} from './document.models'; -import {NestedTreeControl} from '@angular/cdk/tree'; -import {MatTreeNestedDataSource} from '@angular/material/tree'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { DocumentViewerComponent } from './document-viewer/document-viewer.component'; -import {PhaseRunnerComponent} from './phase-runner/phase-runner.component'; @Component({ selector: 'app-document', @@ -33,81 +27,23 @@ import {PhaseRunnerComponent} from './phase-runner/phase-runner.component'; export class DocumentComponent implements WSReceiver { className = this.constructor.name; statusMsg: string; - loading: boolean; - running: boolean; - isOpen: boolean; - phaseViewerRef: MatDialogRef; - phaseRunnerRef: MatDialogRef; type = 'ctl'; component = 'document'; activeLink = 'overview'; - targetPath: string; - phaseTree: KustomNode[] = []; - - treeControl = new NestedTreeControl(node => node.children); - dataSource = new MatTreeNestedDataSource(); - - currentDocId: string; - - showEditor: boolean; - saveBtnDisabled = true; - editorOptions = {language: 'yaml', automaticLayout: true, value: '', theme: 'airshipTheme'}; - code: string; - editorTitle: string; - editorSubtitle: string; - - hasChild = (_: number, node: KustomNode) => !!node.children && node.children.length > 0; - - onInit(editor): void { - editor.onDidChangeModelContent(() => { - this.saveBtnDisabled = false; - }); - } - - constructor(private websocketService: WebsocketService, public dialog: MatDialog) { + constructor(private websocketService: WebsocketService) { this.websocketService.registerFunctions(this); - this.getTarget(); - this.getPhaseTree(); // load the source first } public async receiver(message: WebsocketMessage): Promise { if (message.hasOwnProperty('error')) { this.websocketService.printIfToast(message); - this.loading = false; } else { switch (message.subComponent) { - case 'getTarget': - this.targetPath = message.message; - break; case 'docPull': this.statusMsg = 'Message pull was a ' + message.message; break; - case 'getPhaseTree': - this.handleGetPhaseTree(message.data); - break; - case 'getPhase': - this.handleGetPhase(message); - break; - case 'getYaml': - this.handleGetYaml(message); - break; - case 'getDocumentsBySelector': - this.handleGetDocumentsBySelector(message); - break; - case 'getExecutorDoc': - this.handleGetExecutorDoc(message); - break; - case 'yamlWrite': - this.handleYamlWrite(message); - break; - case 'validatePhase': - this.handleValidatePhase(message); - break; - case 'run': - this.handleRunPhase(message); - break; default: Log.Error(new LogMessage('Document message sub component not handled', this.className, message)); break; @@ -115,181 +51,6 @@ export class DocumentComponent implements WSReceiver { } } - handleValidatePhase(message: WebsocketMessage): void { - this.websocketService.printIfToast(message); - } - - handleRunPhase(message: WebsocketMessage): void { - this.running = false; - this.websocketService.printIfToast(message); - } - - handleGetPhaseTree(data: JSON): void { - this.loading = false; - Object.assign(this.phaseTree, data); - this.dataSource.data = this.phaseTree; - } - - handleGetPhase(message: WebsocketMessage): void { - this.loading = false; - let yaml = ''; - if (message.yaml !== '' && message.yaml !== undefined) { - yaml = atob(message.yaml); - } - this.phaseViewerRef = this.openPhaseDialog(message.id, message.name, message.details, yaml); - } - - handleGetExecutorDoc(message: WebsocketMessage): void { - this.phaseViewerRef.componentInstance.executorYaml = atob(message.yaml); - } - - handleGetDocumentsBySelector(message: WebsocketMessage): void { - this.phaseViewerRef.componentInstance.loading = false; - Object.assign(this.phaseViewerRef.componentInstance.results, message.data); - this.phaseViewerRef.componentInstance.resultsMsg = `Matches: ${this.phaseViewerRef.componentInstance.results.length}`; - } - - handleGetYaml(message: WebsocketMessage): void { - if (message.message === 'rendered') { - this.phaseViewerRef.componentInstance.yaml = atob(message.yaml); - } else { - this.changeEditorContents((message.yaml)); - this.setTitle(message.name); - this.showEditor = true; - this.currentDocId = message.id; - } - } - - handleYamlWrite(message: WebsocketMessage): void { - this.changeEditorContents((message.yaml)); - this.setTitle(message.name); - this.currentDocId = message.id; - this.websocketService.printIfToast(message); - } - - setTitle(name: string): void { - this.editorSubtitle = name; - const str = name.split('/'); - this.editorTitle = str[str.length - 1]; - } - - changeEditorContents(yaml: string): void { - this.code = atob(yaml); - } - - saveYaml(): void { - const websocketMessage = this.constructDocumentWsMessage('yamlWrite'); - websocketMessage.id = this.currentDocId; - websocketMessage.name = this.editorTitle; - websocketMessage.yaml = btoa(this.code); - this.websocketService.sendMessage(websocketMessage); - } - - getPhaseTree(): void { - this.loading = true; - const websocketMessage = this.constructDocumentWsMessage('getPhaseTree'); - this.websocketService.sendMessage(websocketMessage); - } - - openPhaseDialog(id: string, name: string, details: string, yaml: string): MatDialogRef { - const dialogRef = this.dialog.open(DocumentViewerComponent, { - width: '80vw', - height: '90vh', - }); - - dialogRef.componentInstance.id = id; - dialogRef.componentInstance.name = name; - dialogRef.componentInstance.yaml = yaml; - - if (details === '' || details === undefined) { - details = '(Phase details not provided)'; - } - - dialogRef.componentInstance.phaseDetails = details; - - this.getExecutorDoc(JSON.parse(id)); - return dialogRef; - } - - confirmRunPhase(node: KustomNode): void { - const dialogRef = this.dialog.open(PhaseRunnerComponent, { - width: '25vw', - height: '30vh', - data: { - id: node.phaseid, - name: node.name, - options: new RunOptions() - } - }); - - dialogRef.afterClosed().subscribe(result => { - if (result !== undefined) { - const runOpts: RunOptions = result.options; - this.runPhase(node, runOpts); - } - }); - - } - - getPhaseDetails(id: object): void { - const msg = this.constructDocumentWsMessage('getPhaseDetails'); - msg.id = JSON.stringify(id); - this.websocketService.sendMessage(msg); - } - - getPhase(id: object): void { - this.loading = true; - const msg = this.constructDocumentWsMessage('getPhase'); - msg.id = JSON.stringify(id); - this.websocketService.sendMessage(msg); - } - - getYaml(id: string): void { - this.code = null; - const msg = new WebsocketMessage('ctl', 'document', 'getYaml'); - msg.id = id; - msg.message = 'source'; - this.websocketService.sendMessage(msg); - } - - getExecutorDoc(id: object): void { - const msg = this.constructDocumentWsMessage('getExecutorDoc'); - msg.id = JSON.stringify(id); - this.websocketService.sendMessage(msg); - } - - closeEditor(): void { - this.code = null; - this.showEditor = false; - } - - getTarget(): void { - const websocketMessage = this.constructDocumentWsMessage('getTarget'); - this.websocketService.sendMessage(websocketMessage); - } - - // TODO(mfuller): we'll probably want to run / check phase validation - // before actually running the phase - runPhase(node: KustomNode, opts: RunOptions): void { - this.running = true; - const msg = new WebsocketMessage(this.type, 'phase', 'run'); - msg.id = JSON.stringify(node.phaseid); - if (opts !== undefined) { - msg.data = JSON.parse(JSON.stringify(opts)); - } - this.websocketService.sendMessage(msg); - } - - validatePhase(id: object): void { - const msg = new WebsocketMessage(this.type, 'phase', 'validatePhase'); - msg.id = JSON.stringify(id); - this.websocketService.sendMessage(msg); - } - - constructDocumentWsMessage(subComponent: string): WebsocketMessage { - return new WebsocketMessage(this.type, this.component, subComponent); - } - documentPull(): void { this.websocketService.sendMessage(new WebsocketMessage(this.type, this.component, 'pull')); } diff --git a/client/src/app/ctl/document/document.models.ts b/client/src/app/ctl/document/document.models.ts deleted file mode 100644 index 8e968a6..0000000 --- a/client/src/app/ctl/document/document.models.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -# 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 class KustomNode { - id: string; - phaseid: { name: string, namespace: string}; - name: string; - canLoadChildren: boolean; - children: KustomNode[]; - isPhaseNode: boolean; - hasError: boolean; -} - -export class RunOptions { - Debug: boolean; - DryRun: boolean; -} diff --git a/client/src/app/ctl/document/document.module.ts b/client/src/app/ctl/document/document.module.ts index 83914df..549f3e5 100644 --- a/client/src/app/ctl/document/document.module.ts +++ b/client/src/app/ctl/document/document.module.ts @@ -12,54 +12,19 @@ # limitations under the License. */ -import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {NgModule} from '@angular/core'; import {MatTabsModule} from '@angular/material/tabs'; import {DocumentComponent} from './document.component'; -import {MatTreeModule} from '@angular/material/tree'; import {MatButtonModule} from '@angular/material/button'; -import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {MatIconModule} from '@angular/material/icon'; -import {MonacoEditorModule} from 'ngx-monaco-editor'; -import {FormsModule} from '@angular/forms'; -import {ToastrModule} from 'ngx-toastr'; -import {CommonModule} from '@angular/common'; -import {MatProgressBarModule} from '@angular/material/progress-bar'; -import {MatCardModule} from '@angular/material/card'; -import {MatTooltipModule} from '@angular/material/tooltip/'; -import {MatMenuModule} from '@angular/material/menu'; -import {DocumentViewerModule} from './document-viewer/document-viewer.module'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatListModule } from '@angular/material/list'; -import { PhaseRunnerModule } from './phase-runner/phase-runner.module'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ declarations: [ DocumentComponent, ], imports: [ - CommonModule, MatTabsModule, - MatTreeModule, MatButtonModule, - MatButtonToggleModule, - MatIconModule, - MonacoEditorModule, - FormsModule, - ToastrModule, - MatProgressBarModule, - MatCardModule, - MatTooltipModule, - MatMenuModule, - DocumentViewerModule, - MatDialogModule, - MatListModule, - PhaseRunnerModule, - MatInputModule, - MatProgressSpinnerModule ], providers: [], - schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class DocumentModule {} diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.component.css b/client/src/app/ctl/document/phase-runner/phase-runner.component.css deleted file mode 100644 index 3980d58..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.component.css +++ /dev/null @@ -1,22 +0,0 @@ -/* -# 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. -*/ - -.checkbox-wrapper { - min-height: 50px; - display: flex; -} - -mat-checkbox{ - align-self: center; -} \ No newline at end of file diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.component.html b/client/src/app/ctl/document/phase-runner/phase-runner.component.html deleted file mode 100644 index c5e90f8..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.component.html +++ /dev/null @@ -1,12 +0,0 @@ -

    {{data.name}}

    -
    -
    -
    - {{box.name}} -
    -
    -
    -
    - - -
    \ No newline at end of file diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.component.spec.ts b/client/src/app/ctl/document/phase-runner/phase-runner.component.spec.ts deleted file mode 100644 index 229652c..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.component.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -# 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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { PhaseRunnerComponent } from './phase-runner.component'; -import { CommonModule } from '@angular/common'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatInputModule } from '@angular/material/input'; - -describe('PhaseRunnerComponent', () => { - let component: PhaseRunnerComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - CommonModule, - MatDialogModule, - FormsModule, - MatCheckboxModule, - MatInputModule - ], - declarations: [ PhaseRunnerComponent ], - providers: [ { provide: MatDialogRef, useValue: {} }, - { provide: MAT_DIALOG_DATA, useValue: {} } - ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PhaseRunnerComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.component.ts b/client/src/app/ctl/document/phase-runner/phase-runner.component.ts deleted file mode 100644 index 040444f..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* -# 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 { Component, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { RunOptions } from '../document.models'; -import { RunnerDialogData } from './phase-runner.models'; - -@Component({ - selector: 'app-phase-runner', - templateUrl: './phase-runner.component.html', - styleUrls: ['./phase-runner.component.css'] -}) -export class PhaseRunnerComponent { - name: string; - runOpts: RunOptions; - - checkboxes = [ - { name: 'Debug', checked: false}, - { name: 'DryRun', checked: false} - ]; - - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: RunnerDialogData) { - this.name = data.name; - this.runOpts = data.options; - } - - onNoClick(): void { - this.dialogRef.close(); - } - - getChecked(): void { - this.runOpts.Debug = this.checkboxes[0].checked; - this.runOpts.DryRun = this.checkboxes[1].checked; - } -} diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.models.ts b/client/src/app/ctl/document/phase-runner/phase-runner.models.ts deleted file mode 100644 index 69aa249..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.models.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -# 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 {RunOptions} from '../document.models'; - -export class RunnerDialogData { - name: string; - id: string; - options: RunOptions; -} diff --git a/client/src/app/ctl/document/phase-runner/phase-runner.module.ts b/client/src/app/ctl/document/phase-runner/phase-runner.module.ts deleted file mode 100644 index dec2b72..0000000 --- a/client/src/app/ctl/document/phase-runner/phase-runner.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -# 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 {NgModule} from '@angular/core'; -import {PhaseRunnerComponent} from '../phase-runner/phase-runner.component'; -import { MatDialogModule } from '@angular/material/dialog'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import {MatCheckboxModule} from '@angular/material/checkbox'; -import { CommonModule } from '@angular/common'; -import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; - -@NgModule({ - declarations: [ - PhaseRunnerComponent - ], - imports: [ - CommonModule, - MatDialogModule, - FormsModule, - MatCheckboxModule, - ReactiveFormsModule, - MatInputModule, - MatButtonModule - ], - providers: [], -}) -export class PhaseRunnerModule {} diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.component.css b/client/src/app/ctl/phase/phase-runner/phase-runner.component.css new file mode 100644 index 0000000..3980d58 --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.component.css @@ -0,0 +1,22 @@ +/* +# 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. +*/ + +.checkbox-wrapper { + min-height: 50px; + display: flex; +} + +mat-checkbox{ + align-self: center; +} \ No newline at end of file diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.component.html b/client/src/app/ctl/phase/phase-runner/phase-runner.component.html new file mode 100644 index 0000000..c5e90f8 --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.component.html @@ -0,0 +1,12 @@ +

    {{data.name}}

    +
    +
    +
    + {{box.name}} +
    +
    +
    +
    + + +
    \ No newline at end of file diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.component.spec.ts b/client/src/app/ctl/phase/phase-runner/phase-runner.component.spec.ts new file mode 100644 index 0000000..229652c --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.component.spec.ts @@ -0,0 +1,53 @@ +/* +# 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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { PhaseRunnerComponent } from './phase-runner.component'; +import { CommonModule } from '@angular/common'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; + +describe('PhaseRunnerComponent', () => { + let component: PhaseRunnerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + MatDialogModule, + FormsModule, + MatCheckboxModule, + MatInputModule + ], + declarations: [ PhaseRunnerComponent ], + providers: [ { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PhaseRunnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.component.ts b/client/src/app/ctl/phase/phase-runner/phase-runner.component.ts new file mode 100644 index 0000000..e5f0ed8 --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.component.ts @@ -0,0 +1,49 @@ +/* +# 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 { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { RunOptions } from '../phase.models'; +import { RunnerDialogData } from './phase-runner.models'; + +@Component({ + selector: 'app-phase-runner', + templateUrl: './phase-runner.component.html', + styleUrls: ['./phase-runner.component.css'] +}) +export class PhaseRunnerComponent { + name: string; + runOpts: RunOptions; + + checkboxes = [ + { name: 'Debug', checked: false}, + { name: 'DryRun', checked: false} + ]; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RunnerDialogData) { + this.name = data.name; + this.runOpts = data.options; + } + + onNoClick(): void { + this.dialogRef.close(); + } + + getChecked(): void { + this.runOpts.Debug = this.checkboxes[0].checked; + this.runOpts.DryRun = this.checkboxes[1].checked; + } +} diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.models.ts b/client/src/app/ctl/phase/phase-runner/phase-runner.models.ts new file mode 100644 index 0000000..9114ca4 --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.models.ts @@ -0,0 +1,21 @@ +/* +# 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 {RunOptions} from '../phase.models'; + +export class RunnerDialogData { + name: string; + id: string; + options: RunOptions; +} diff --git a/client/src/app/ctl/phase/phase-runner/phase-runner.module.ts b/client/src/app/ctl/phase/phase-runner/phase-runner.module.ts new file mode 100644 index 0000000..dec2b72 --- /dev/null +++ b/client/src/app/ctl/phase/phase-runner/phase-runner.module.ts @@ -0,0 +1,39 @@ +/* +# 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 {NgModule} from '@angular/core'; +import {PhaseRunnerComponent} from '../phase-runner/phase-runner.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import { CommonModule } from '@angular/common'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; + +@NgModule({ + declarations: [ + PhaseRunnerComponent + ], + imports: [ + CommonModule, + MatDialogModule, + FormsModule, + MatCheckboxModule, + ReactiveFormsModule, + MatInputModule, + MatButtonModule + ], + providers: [], +}) +export class PhaseRunnerModule {} diff --git a/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.css b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.css new file mode 100644 index 0000000..a650336 --- /dev/null +++ b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.css @@ -0,0 +1,69 @@ +/* +# 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. +*/ + +.viewer-editor { + height: inherit; + width: inherit; +} + +ngx-monaco-editor { + width: 100%; + height: 100%; +} + +.filter-param { + height: 25px; + padding-left: 5px; +} + +h4 { + color: blue; +} + +.result-items { + height: 100%; + width: 40%; + top: 48px; + overflow: auto; +} + +.rendered-container { + display: flex; + flex-direction: row; + width: 100%; + height: 78vh; +} + +.editor-div { + width: 60%; + height: 100%; +} + +fieldset { + border: 0; +} + +.spacer { + flex: 1 1 auto; +} + +.apply-div { + display: flex; + flex-direction: row; +} + +.spinner { + margin-left: 15px; + margin-top: 5px; +} \ No newline at end of file diff --git a/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.html b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.html new file mode 100644 index 0000000..90b2e93 --- /dev/null +++ b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.html @@ -0,0 +1,92 @@ + + + + Phase '{{name}}' + + + + + +
    +
    +
    +

    Filter documents by:

    +

    + + Name + + +

    +

    + + Namespace + + +

    +

    + + GVK + + +

    +

    + + Kind + + +

    +

    + + Label + + +

    +

    + + Annotation + + +

    +
    + + +
    +
    +
    +
    +
    + +
    +
    + + + + + + + + +
    +
    + +
    +
    +
    +
    diff --git a/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.ts b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.ts new file mode 100644 index 0000000..c777924 --- /dev/null +++ b/client/src/app/ctl/phase/phase-viewer/phase-viewer.component.ts @@ -0,0 +1,119 @@ +/* +# 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 {Component, OnInit} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {KustomNode} from '../phase.models'; +import {WebsocketMessage} from '../../../../services/websocket/websocket.models'; +import {WebsocketService} from '../../../../services/websocket/websocket.service'; +import {FormControl, FormGroup} from '@angular/forms'; + +@Component({ + selector: 'app-phase-viewer', + templateUrl: 'phase-viewer.component.html', + styleUrls: ['./phase-viewer.component.css'] + +}) + +export class PhaseViewerComponent implements OnInit { + editorOptions = {language: 'yaml', automaticLayout: true, readOnly: true, theme: 'airshipTheme'}; + bundleYaml: string; + executorYaml: string; + phaseDetails: string; + loading: boolean; + resultsMsg = ''; + + results: KustomNode[] = []; + id: string; + name: string; + yaml: string; + + filterOptions = new FormGroup({ + name: new FormControl(''), + namespace: new FormControl(''), + gvk: new FormControl(''), + kind: new FormControl(''), + label: new FormControl(''), + annotation: new FormControl('') + }); + + constructor( + public dialogRef: MatDialogRef, + private websocketService: WebsocketService) {} + + ngOnInit(): void { + this.bundleYaml = this.yaml; + if (this.bundleYaml !== '') { + this.getDocumentsBySelector('{}'); + } + this.yaml = this.phaseDetails; + } + + onClose(): void { + this.dialogRef.close(); + this.results = null; + } + + setModel(val: string): void { + switch (val) { + case 'bundle': + this.yaml = this.bundleYaml; + break; + case 'executor': + this.yaml = this.executorYaml; + break; + case 'details': + this.yaml = this.phaseDetails; + break; + } + } + + getDocumentsBySelector(selector: string): void { + const msg = new WebsocketMessage('ctl', 'phase', 'getDocumentsBySelector'); + msg.message = selector; + msg.id = this.id; + this.websocketService.sendMessage(msg); + } + + getYaml(id: string): void { + this.yaml = null; + const msg = new WebsocketMessage('ctl', 'phase', 'getYaml'); + msg.id = id; + msg.message = 'rendered'; + this.websocketService.sendMessage(msg); + } + + onSubmit(data: any): void { + this.loading = true; + this.results = []; + this.resultsMsg = ''; + const selector = {}; + Object.keys(this.filterOptions.controls).forEach(key => { + if (this.filterOptions.controls[key].value !== '') { + if (key === 'gvk') { + const str: string = this.filterOptions.controls[key].value; + const arr = str.split(' '); + selector[key] = { + group: arr[0], + version: arr[1], + kind: arr[2] + }; + } else { + selector[key] = this.filterOptions.controls[key].value; + } + } + }); + this.getDocumentsBySelector(JSON.stringify(selector)); + } +} diff --git a/client/src/app/ctl/phase/phase-viewer/phase-viewer.module.ts b/client/src/app/ctl/phase/phase-viewer/phase-viewer.module.ts new file mode 100644 index 0000000..87b66e8 --- /dev/null +++ b/client/src/app/ctl/phase/phase-viewer/phase-viewer.module.ts @@ -0,0 +1,54 @@ +/* +# 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 {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatListModule } from '@angular/material/list'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatGridListModule } from '@angular/material/grid-list'; +import {MatInputModule} from '@angular/material/input'; +import {MonacoEditorModule} from 'ngx-monaco-editor'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { PhaseViewerComponent } from './phase-viewer.component'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; + +@NgModule({ + declarations: [ + PhaseViewerComponent + ], + imports: [ + CommonModule, + MatFormFieldModule, + MatListModule, + MatIconModule, + MatButtonModule, + FormsModule, + ReactiveFormsModule, + MatGridListModule, + MatFormFieldModule, + MatInputModule, + MonacoEditorModule, + MatProgressSpinnerModule, + MatSidenavModule, + MatToolbarModule, + MatTooltipModule + ], + providers: [], +}) +export class PhaseViewerModule {} diff --git a/client/src/app/ctl/phase/phase.component.css b/client/src/app/ctl/phase/phase.component.css new file mode 100644 index 0000000..2869f96 --- /dev/null +++ b/client/src/app/ctl/phase/phase.component.css @@ -0,0 +1,120 @@ +/* +# 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. +*/ + +.explorer-container { + display: flex; + flex-direction: row; + height: 100%; +} + +/* Phase Tree styles */ + +.grey-icon { + color: grey; + height: 20px; + width: 20px; +} + +.error-icon { + padding-left: 20px; + color: red; + height: 20px; + width: 20px; +} + +.docless-phase { + padding-left: 20px; +} + +.docless-phase-btn { + padding-left: 20px; + padding-right: 0px; +} + +.phase-tree-invisible { + display: none; +} + +.mat-tree-node { + min-height: 25px; +} + +.nested-ul { + padding-left: 0px; +} + +.phase-tree ul, +.phase-tree li { + padding-left: 20px; + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +.phase-tree { + height: 100%; +} + +.phase-tree-card { + height: calc(100vh - 176px); + width: 40%; + box-sizing: border-box; +} + +.phase-card-content { + max-height: 90%; + box-sizing: border-box; + overflow-y: auto; +} + +.phase-tree-progress-bar { + margin-left: 5px; + margin-right: 5px; +} + +.phase-tree-toggle { + padding-right: 0px; +} + +/* Editor styles */ +.editor-btn { + margin-right: 5px; + margin-left: 5px; +} + +.editor-button-grp { + display: flex; +} + +.editor-btns { + margin-left: auto; + margin-right: 30px; +} + +.editor-card { + height: calc(100vh - 176px); + width: 60%; + box-sizing: border-box; +} + +.editor-card-content { + height: 85%; + width: 100%; + margin-bottom: 0px; +} + +ngx-monaco-editor { + height: 100%; + width: 100%; +} diff --git a/client/src/app/ctl/phase/phase.component.html b/client/src/app/ctl/phase/phase.component.html index f644e08..59e4e1f 100755 --- a/client/src/app/ctl/phase/phase.component.html +++ b/client/src/app/ctl/phase/phase.component.html @@ -1 +1,104 @@ -

    Phase component

    \ No newline at end of file + + +
    + + + Phases + Target: {{targetPath}} + + + + + +
  • +
    +
    + +
    +
    + +
    + + + + + + + +
    +
    +
    +
  • +
    + +
  • +
    + + + + + + + +
    +
      + +
    +
  • +
    +
    +
    +
    + + + + {{editorTitle}} + {{editorSubtitle}} + + + + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    diff --git a/client/src/app/ctl/phase/phase.component.spec.ts b/client/src/app/ctl/phase/phase.component.spec.ts index 4e25dd3..3e0e284 100755 --- a/client/src/app/ctl/phase/phase.component.spec.ts +++ b/client/src/app/ctl/phase/phase.component.spec.ts @@ -12,24 +12,48 @@ # limitations under the License. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { PhaseComponent } from './phase.component'; -import { ToastrModule } from 'ngx-toastr'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {PhaseComponent} from './phase.component'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTreeModule} from '@angular/material/tree'; +import {MatButtonModule} from '@angular/material/button'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatIconModule} from '@angular/material/icon'; +import {MonacoEditorModule} from 'ngx-monaco-editor'; +import {FormsModule} from '@angular/forms'; +import {ToastrModule} from 'ngx-toastr'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatCardModule} from '@angular/material/card'; +import {MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; -describe('PhaseComponent', () => { +describe('DocumentComponent', () => { let component: PhaseComponent; let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - ToastrModule.forRoot() + BrowserAnimationsModule, + MatTabsModule, + MatTreeModule, + MatButtonModule, + MatButtonToggleModule, + MatIconModule, + MonacoEditorModule, + FormsModule, + ToastrModule.forRoot(), + MatCardModule, + MatProgressBarModule, + MatTooltipModule, + MatDialogModule, + MatMenuModule ], - declarations: [ - PhaseComponent - ] + declarations: [PhaseComponent] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/client/src/app/ctl/phase/phase.component.ts b/client/src/app/ctl/phase/phase.component.ts index 2d098d9..7210233 100755 --- a/client/src/app/ctl/phase/phase.component.ts +++ b/client/src/app/ctl/phase/phase.component.ts @@ -12,33 +12,280 @@ # limitations under the License. */ -import { Component } from '@angular/core'; -import { WebsocketService } from '../../../services/websocket/websocket.service'; -import { WebsocketMessage, WSReceiver } from '../../../services/websocket/websocket.models'; -import { Log } from '../../../services/log/log.service'; -import { LogMessage } from '../../../services/log/log-message'; +import {Component} from '@angular/core'; +import {WebsocketService} from '../../../services/websocket/websocket.service'; +import {WebsocketMessage, WSReceiver} from '../../../services/websocket/websocket.models'; +import {Log} from '../../../services/log/log.service'; +import {LogMessage} from '../../../services/log/log-message'; +import {KustomNode, RunOptions} from './phase.models'; +import {NestedTreeControl} from '@angular/cdk/tree'; +import {MatTreeNestedDataSource} from '@angular/material/tree'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { PhaseViewerComponent } from './phase-viewer/phase-viewer.component'; +import {PhaseRunnerComponent} from './phase-runner/phase-runner.component'; @Component({ - selector: 'app-bare-metal', + selector: 'app-phase', templateUrl: './phase.component.html', + styleUrls: ['./phase.component.css'] }) export class PhaseComponent implements WSReceiver { className = this.constructor.name; - // TODO (aschiefe): extract these strings to constants + statusMsg: string; + loading: boolean; + running: boolean; + isOpen: boolean; + phaseViewerRef: MatDialogRef; + phaseRunnerRef: MatDialogRef; + type = 'ctl'; component = 'phase'; + activeLink = 'overview'; + + targetPath: string; + phaseTree: KustomNode[] = []; + + treeControl = new NestedTreeControl(node => node.children); + dataSource = new MatTreeNestedDataSource(); + + currentDocId: string; + + showEditor: boolean; + saveBtnDisabled = true; + editorOptions = {language: 'yaml', automaticLayout: true, value: '', theme: 'airshipTheme'}; + code: string; + editorTitle: string; + editorSubtitle: string; - constructor(private websocketService: WebsocketService) { + hasChild = (_: number, node: KustomNode) => !!node.children && node.children.length > 0; + + onInit(editor): void { + editor.onDidChangeModelContent(() => { + this.saveBtnDisabled = false; + }); + } + + constructor(private websocketService: WebsocketService, public dialog: MatDialog) { this.websocketService.registerFunctions(this); + this.getTarget(); + this.getPhaseTree(); // load the source first } - async receiver(message: WebsocketMessage): Promise { + public async receiver(message: WebsocketMessage): Promise { if (message.hasOwnProperty('error')) { this.websocketService.printIfToast(message); + this.loading = false; } else { - // TODO (aschiefe): determine what should be notifications and what should be 86ed - Log.Debug(new LogMessage('Message received in image', this.className, message)); + switch (message.subComponent) { + case 'getTarget': + this.targetPath = message.message; + break; + case 'docPull': + this.statusMsg = 'Message pull was a ' + message.message; + break; + case 'getPhaseTree': + this.handleGetPhaseTree(message.data); + break; + case 'getPhase': + this.handleGetPhase(message); + break; + case 'getYaml': + this.handleGetYaml(message); + break; + case 'getDocumentsBySelector': + this.handleGetDocumentsBySelector(message); + break; + case 'getExecutorDoc': + this.handleGetExecutorDoc(message); + break; + case 'yamlWrite': + this.handleYamlWrite(message); + break; + case 'validatePhase': + this.handleValidatePhase(message); + break; + case 'run': + this.handleRunPhase(message); + break; + default: + Log.Error(new LogMessage('Phase message sub component not handled', this.className, message)); + break; + } + } + } + + handleValidatePhase(message: WebsocketMessage): void { + this.websocketService.printIfToast(message); + } + + handleRunPhase(message: WebsocketMessage): void { + this.running = false; + this.websocketService.printIfToast(message); + } + + handleGetPhaseTree(data: JSON): void { + this.loading = false; + Object.assign(this.phaseTree, data); + this.dataSource.data = this.phaseTree; + } + + handleGetPhase(message: WebsocketMessage): void { + this.loading = false; + let yaml = ''; + if (message.yaml !== '' && message.yaml !== undefined) { + yaml = atob(message.yaml); } + this.phaseViewerRef = this.openPhaseDialog(message.id, message.name, message.details, yaml); + } + + handleGetExecutorDoc(message: WebsocketMessage): void { + this.phaseViewerRef.componentInstance.executorYaml = atob(message.yaml); + } + + handleGetDocumentsBySelector(message: WebsocketMessage): void { + this.phaseViewerRef.componentInstance.loading = false; + Object.assign(this.phaseViewerRef.componentInstance.results, message.data); + } + + handleGetYaml(message: WebsocketMessage): void { + if (message.message === 'rendered') { + this.phaseViewerRef.componentInstance.yaml = atob(message.yaml); + } else { + this.changeEditorContents((message.yaml)); + this.setTitle(message.name); + this.showEditor = true; + this.currentDocId = message.id; + } + } + + handleYamlWrite(message: WebsocketMessage): void { + this.changeEditorContents((message.yaml)); + this.setTitle(message.name); + this.currentDocId = message.id; + this.websocketService.printIfToast(message); + } + + setTitle(name: string): void { + this.editorSubtitle = name; + const str = name.split('/'); + this.editorTitle = str[str.length - 1]; + } + + changeEditorContents(yaml: string): void { + this.code = atob(yaml); + } + + saveYaml(): void { + const websocketMessage = this.newMessage('yamlWrite'); + websocketMessage.id = this.currentDocId; + websocketMessage.name = this.editorTitle; + websocketMessage.yaml = btoa(this.code); + this.websocketService.sendMessage(websocketMessage); + } + + getPhaseTree(): void { + this.loading = true; + const websocketMessage = this.newMessage('getPhaseTree'); + this.websocketService.sendMessage(websocketMessage); + } + + openPhaseDialog(id: string, name: string, details: string, yaml: string): MatDialogRef { + const dialogRef = this.dialog.open(PhaseViewerComponent, { + width: '80vw', + height: '90vh', + }); + + dialogRef.componentInstance.id = id; + dialogRef.componentInstance.name = name; + dialogRef.componentInstance.yaml = yaml; + + if (details === '' || details === undefined) { + details = '(Phase details not provided)'; + } + + dialogRef.componentInstance.phaseDetails = details; + + this.getExecutorDoc(JSON.parse(id)); + return dialogRef; + } + + confirmRunPhase(node: KustomNode): void { + const dialogRef = this.dialog.open(PhaseRunnerComponent, { + width: '25vw', + height: '30vh', + data: { + id: node.phaseid, + name: node.name, + options: new RunOptions() + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + const runOpts: RunOptions = result.options; + this.runPhase(node, runOpts); + } + }); + + } + + getPhaseDetails(id: object): void { + const msg = this.newMessage('getPhaseDetails'); + msg.id = JSON.stringify(id); + this.websocketService.sendMessage(msg); + } + + getPhase(id: object): void { + this.loading = true; + const msg = this.newMessage('getPhase'); + msg.id = JSON.stringify(id); + this.websocketService.sendMessage(msg); + } + + getYaml(id: string): void { + this.code = null; + const msg = this.newMessage('getYaml'); + msg.id = id; + msg.message = 'source'; + this.websocketService.sendMessage(msg); + } + + getExecutorDoc(id: object): void { + const msg = this.newMessage('getExecutorDoc'); + msg.id = JSON.stringify(id); + this.websocketService.sendMessage(msg); + } + + closeEditor(): void { + this.code = null; + this.showEditor = false; + } + + getTarget(): void { + const websocketMessage = this.newMessage('getTarget'); + this.websocketService.sendMessage(websocketMessage); + } + + // TODO(mfuller): we'll probably want to run / check phase validation + // before actually running the phase + runPhase(node: KustomNode, opts: RunOptions): void { + this.running = true; + const msg = this.newMessage('run'); + msg.id = JSON.stringify(node.phaseid); + if (opts !== undefined) { + msg.data = JSON.parse(JSON.stringify(opts)); + } + this.websocketService.sendMessage(msg); + } + + validatePhase(id: object): void { + const msg = this.newMessage('validatePhase'); + msg.id = JSON.stringify(id); + this.websocketService.sendMessage(msg); + } + + newMessage(subComponent: string): WebsocketMessage { + return new WebsocketMessage(this.type, this.component, subComponent); } } diff --git a/client/src/app/ctl/phase/phase.models.ts b/client/src/app/ctl/phase/phase.models.ts new file mode 100644 index 0000000..8e968a6 --- /dev/null +++ b/client/src/app/ctl/phase/phase.models.ts @@ -0,0 +1,28 @@ +/* +# 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 class KustomNode { + id: string; + phaseid: { name: string, namespace: string}; + name: string; + canLoadChildren: boolean; + children: KustomNode[]; + isPhaseNode: boolean; + hasError: boolean; +} + +export class RunOptions { + Debug: boolean; + DryRun: boolean; +} diff --git a/client/src/app/ctl/phase/phase.module.ts b/client/src/app/ctl/phase/phase.module.ts index e7e3656..809160d 100755 --- a/client/src/app/ctl/phase/phase.module.ts +++ b/client/src/app/ctl/phase/phase.module.ts @@ -12,15 +12,54 @@ # limitations under the License. */ -import { NgModule } from '@angular/core'; -import { PhaseComponent } from './phase.component'; +import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {MatTabsModule} from '@angular/material/tabs'; +import {PhaseComponent} from './phase.component'; +import {MatTreeModule} from '@angular/material/tree'; +import {MatButtonModule} from '@angular/material/button'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatIconModule} from '@angular/material/icon'; +import {MonacoEditorModule} from 'ngx-monaco-editor'; +import {FormsModule} from '@angular/forms'; +import {ToastrModule} from 'ngx-toastr'; +import {CommonModule} from '@angular/common'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatCardModule} from '@angular/material/card'; +import {MatTooltipModule} from '@angular/material/tooltip/'; +import {MatMenuModule} from '@angular/material/menu'; +import {PhaseViewerModule} from './phase-viewer/phase-viewer.module'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatListModule } from '@angular/material/list'; +import { PhaseRunnerModule } from './phase-runner/phase-runner.module'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @NgModule({ - imports: [ - ], declarations: [ - PhaseComponent + PhaseComponent, + ], + imports: [ + CommonModule, + MatTabsModule, + MatTreeModule, + MatButtonModule, + MatButtonToggleModule, + MatIconModule, + MonacoEditorModule, + FormsModule, + ToastrModule, + MatProgressBarModule, + MatCardModule, + MatTooltipModule, + MatMenuModule, + PhaseViewerModule, + MatDialogModule, + MatListModule, + PhaseRunnerModule, + MatInputModule, + MatProgressSpinnerModule ], - providers: [] + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) -export class PhaseModule { } +export class PhaseModule {} diff --git a/pkg/ctl/document.go b/pkg/ctl/document.go index 47b85aa..d1d1727 100644 --- a/pkg/ctl/document.go +++ b/pkg/ctl/document.go @@ -15,29 +15,13 @@ package ctl import ( - "bytes" - "encoding/base64" - "encoding/json" - "errors" "fmt" - "io/ioutil" - "os" - "path/filepath" - "github.com/google/uuid" "opendev.org/airship/airshipctl/pkg/config" - "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/document/pull" - "opendev.org/airship/airshipctl/pkg/phase" - "opendev.org/airship/airshipctl/pkg/phase/ifc" "opendev.org/airship/airshipui/pkg/configs" ) -var ( - fileIndex map[string]string - docIndex map[string]document.Document -) - // HandleDocumentRequest will flop between requests so we don't have to have them all mapped as function calls func HandleDocumentRequest(user *string, request configs.WsMessage) configs.WsMessage { response := configs.WsMessage{ @@ -48,7 +32,6 @@ func HandleDocumentRequest(user *string, request configs.WsMessage) configs.WsMe var err error var message *string - var id string client, err := NewClient(AirshipConfigPath, KubeConfigPath, request) if err != nil { @@ -57,37 +40,12 @@ func HandleDocumentRequest(user *string, request configs.WsMessage) configs.WsMe return response } - switch request.SubComponent { + subComponent := request.SubComponent + switch subComponent { case configs.Pull: message, err = client.docPull() case configs.Plugin: err = fmt.Errorf("Subcomponent %s not implemented", request.SubComponent) - case configs.YamlWrite: - id = request.ID - response.Name, response.YAML, err = client.writeYamlFile(id, request.YAML) - s := fmt.Sprintf("File '%s' saved successfully", response.Name) - message = &s - case configs.GetYaml: - id = request.ID - message = request.Message - response.Name, response.YAML, err = client.getYaml(id, *message) - case configs.GetPhaseTree: - response.Data, err = client.GetPhaseTree() - case configs.GetPhase: - id = request.ID - s := "rendered" - message = &s - response.Name, response.Details, response.YAML, err = client.GetPhase(id) - case configs.GetDocumentsBySelector: - id = request.ID - response.Data, err = GetDocumentsBySelector(request.ID, *request.Message) - case configs.GetTarget: - message = client.getTarget() - case configs.GetExecutorDoc: - id = request.ID - s := "rendered" - message = &s - response.Name, response.YAML, err = client.GetExecutorDoc(id) default: err = fmt.Errorf("Subcomponent %s not found", request.SubComponent) } @@ -97,218 +55,11 @@ func HandleDocumentRequest(user *string, request configs.WsMessage) configs.WsMe response.Error = &e } else { response.Message = message - response.ID = id } return response } -// GetExecutorDoc returns the title and YAML of the executor document for the specified phase -func (c *Client) GetExecutorDoc(id string) (string, string, error) { - helper, err := getHelper() - if err != nil { - return "", "", err - } - - phaseID := ifc.ID{} - - err = json.Unmarshal([]byte(id), &phaseID) - if err != nil { - return "", "", err - } - - ed, err := helper.ExecutorDoc(phaseID) - if err != nil { - return "", "", err - } - - title := ed.GetName() - bytes, err := ed.AsYAML() - if err != nil { - return "", "", err - } - - return title, base64.StdEncoding.EncodeToString(bytes), nil -} - -func (c *Client) getTarget() *string { - var s string - m, err := c.Config.CurrentContextManifest() - if err != nil { - s = "unknown" - return &s - } - - s = filepath.Join(m.TargetPath, m.SubPath) - return &s -} - -func (c *Client) getPhaseDetails(id ifc.ID) (string, error) { - helper, err := getHelper() - if err != nil { - return "", err - } - - pClient := phase.NewClient(helper) - - phaseIfc, err := pClient.PhaseByID(id) - if err != nil { - return "", err - } - - return phaseIfc.Details() -} - -func (c *Client) getYaml(id, message string) (string, string, error) { - switch message { - case "source": - name, yaml, err := c.getFileYaml(id) - return name, yaml, err - case "rendered": - name, yaml, err := c.getDocumentYaml(id) - return name, yaml, err - default: - return "", "", fmt.Errorf("'%s' unrecognized document type", message) - } -} - -func (c *Client) getDocumentYaml(id string) (string, string, error) { - doc, ok := docIndex[id] - if !ok { - return "", "", fmt.Errorf("document with ID '%s' not found", id) - } - title := doc.GetName() - bytes, err := doc.AsYAML() - if err != nil { - return "", "", err - } - - return title, base64.StdEncoding.EncodeToString(bytes), nil -} - -func (c *Client) getFileYaml(id string) (string, string, error) { - path, ok := fileIndex[id] - if !ok { - return "", "", fmt.Errorf("file with ID '%s' not found", id) - } - - ccm, err := c.Config.CurrentContextManifest() - if err != nil { - return "", "", err - } - - // this is making the assumption that the site definition - // will always found at: targetPath/subPath - sitePath := filepath.Join(ccm.TargetPath, ccm.SubPath) - - // TODO(mfuller): will this be true in treasuremap or - // other external repos? - manifestsDir := filepath.Join(sitePath, "..", "..") - - title, err := filepath.Rel(manifestsDir, path) - if err != nil { - return "", "", err - } - - file, err := os.Open(path) - if err != nil { - return "", "", err - } - - defer file.Close() - - bytes, err := ioutil.ReadAll(file) - if err != nil { - return "", "", err - } - - return title, base64.StdEncoding.EncodeToString(bytes), nil -} - -func (c *Client) writeYamlFile(id, yaml64 string) (string, string, error) { - path, ok := fileIndex[id] - if !ok { - return "", "", fmt.Errorf("ID %s not found", id) - } - - yaml, err := base64.StdEncoding.DecodeString(yaml64) - if err != nil { - return "", "", err - } - - err = ioutil.WriteFile(path, yaml, 0600) - if err != nil { - return "", "", err - } - - return c.getFileYaml(id) -} - -func getPhaseBundle(id ifc.ID) (document.Bundle, error) { - helper, err := getHelper() - if err != nil { - return nil, err - } - - pClient := phase.NewClient(helper) - - phaseIfc, err := pClient.PhaseByID(id) - if err != nil { - return nil, err - } - - docRoot, err := phaseIfc.DocumentRoot() - if err != nil { - // if phase has no doc entrypoint defined, just - // return nothing; otherwise, return the error - if errors.As(err, &phase.ErrDocumentEntrypointNotDefined{}) { - return nil, nil - } - return nil, err - } - - b, err := document.NewBundleByPath(docRoot) - if err != nil { - return nil, err - } - return b, nil -} - -// GetPhase returns the name, description, and doc bundle for specified phase -func (c *Client) GetPhase(id string) (string, string, string, error) { - phaseID := ifc.ID{} - - err := json.Unmarshal([]byte(id), &phaseID) - if err != nil { - return "", "", "", err - } - - title := phaseID.Name - - details, err := c.getPhaseDetails(phaseID) - if err != nil { - return "", "", "", err - } - - bundle, err := getPhaseBundle(phaseID) - if err != nil { - return "", "", "", err - } - - // only return title if phase has no bundle - if bundle == nil { - return title, details, "", nil - } - - var buf bytes.Buffer - err = bundle.Write(&buf) - if err != nil { - return "", "", "", err - } - - return title, details, base64.StdEncoding.EncodeToString(buf.Bytes()), nil -} - func (c *Client) docPull() (*string, error) { var message *string cfgFactory := config.CreateFactory(AirshipConfigPath, KubeConfigPath) @@ -322,129 +73,3 @@ func (c *Client) docPull() (*string, error) { return message, err } - -// SelectorParams structure to hold data for constructing a document Selector -type SelectorParams struct { - Name string `json:"name,omitempty"` - Namespace string `json:"namespace,omitempty"` - GVK GVK `json:"gvk,omitempty"` - Kind string `json:"kind,omitempty"` - Label string `json:"label,omitempty"` - Annotation string `json:"annotation,omitempty"` -} - -// GVK small structure to hold group, version, kind for building a Selector -type GVK struct { - Group string `json:"group"` - Version string `json:"version"` - Kind string `json:"kind"` -} - -// GetDocumentsBySelector returns a slice of KustomNodes representing all phase -// documents returned by applying the provided Selector -func GetDocumentsBySelector(id string, data string) ([]KustomNode, error) { - docIndex = map[string]document.Document{} - - selector, err := getSelector(data) - if err != nil { - return nil, err - } - - phaseID := ifc.ID{} - err = json.Unmarshal([]byte(id), &phaseID) - if err != nil { - return nil, err - } - - helper, err := getHelper() - if err != nil { - return nil, err - } - - pClient := phase.NewClient(helper) - - phaseIfc, err := pClient.PhaseByID(phaseID) - if err != nil { - return nil, err - } - - docRoot, err := phaseIfc.DocumentRoot() - if err != nil { - return nil, err - } - - bundle, err := document.NewBundleByPath(docRoot) - if err != nil { - return nil, err - } - - docs, err := bundle.Select(selector) - if err != nil { - return nil, err - } - - results := []KustomNode{} - - for _, doc := range docs { - // this is a workaround for a kustomize issue where cluster-scoped objects - // are included in matching results when a namespace selector is specified - // (https://github.com/kubernetes-sigs/kustomize/issues/2248) - if selector.Namespace != "" && selector.Namespace != doc.GetNamespace() { - continue - } - - id := uuid.New().String() - docIndex[id] = doc - - name := doc.GetNamespace() - if name == "" { - name = "[none]" - } - - results = append(results, KustomNode{ - ID: id, - Name: fmt.Sprintf("%s/%s/%s", - name, - doc.GetKind(), - doc.GetName(), - )}, - ) - } - - return results, nil -} - -func getSelector(data string) (document.Selector, error) { - params := SelectorParams{} - err := json.Unmarshal([]byte(data), ¶ms) - if err != nil { - return document.Selector{}, err - } - - s := document.NewSelector() - - // build selector based on what we were given - if params.Name != "" { - s = s.ByName(params.Name) - } - if params.Namespace != "" { - s = s.ByNamespace(params.Namespace) - } - if (GVK{}) != params.GVK { - s = s.ByGvk( - params.GVK.Group, - params.GVK.Version, - params.GVK.Kind, - ) - } - if params.Kind != "" { - s = s.ByKind(params.Kind) - } - if params.Label != "" { - s = s.ByLabel(params.Label) - } - if params.Annotation != "" { - s = s.ByAnnotation(params.Annotation) - } - return s, nil -} diff --git a/pkg/ctl/phase.go b/pkg/ctl/phase.go index 23ce347..b0bc8dc 100644 --- a/pkg/ctl/phase.go +++ b/pkg/ctl/phase.go @@ -15,26 +15,40 @@ package ctl import ( + "bytes" + "encoding/base64" "encoding/json" "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "github.com/google/uuid" + "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/events" "opendev.org/airship/airshipctl/pkg/phase" "opendev.org/airship/airshipctl/pkg/phase/ifc" "opendev.org/airship/airshipui/pkg/configs" ) +var ( + fileIndex map[string]string + docIndex map[string]document.Document +) + // HandlePhaseRequest will flop between requests so we don't have to have them all mapped as function calls // This will wait for the sub component to complete before responding. The assumption is this is an async request func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessage { response := configs.WsMessage{ Type: configs.CTL, - Component: configs.Document, // setting this to Document for now since that's handling phase requests + Component: configs.Phase, SubComponent: request.SubComponent, } var err error var message *string + var id string var valid bool client, err := NewClient(AirshipConfigPath, KubeConfigPath, request) @@ -44,17 +58,39 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa return response } - subComponent := request.SubComponent - switch subComponent { - case configs.Plan: - err = fmt.Errorf("Subcomponent %s not implemented", request.SubComponent) - case configs.Render: - err = fmt.Errorf("Subcomponent %s not implemented", request.SubComponent) + switch request.SubComponent { case configs.Run: err = client.RunPhase(request) case configs.ValidatePhase: valid, err = client.ValidatePhase(request.ID, request.SessionID) message = validateHelper(valid) + case configs.YamlWrite: + id = request.ID + response.Name, response.YAML, err = client.writeYamlFile(id, request.YAML) + s := fmt.Sprintf("File '%s' saved successfully", response.Name) + message = &s + case configs.GetYaml: + id = request.ID + message = request.Message + response.Name, response.YAML, err = client.getYaml(id, *message) + case configs.GetPhaseTree: + response.Data, err = client.GetPhaseTree() + case configs.GetPhase: + id = request.ID + s := "rendered" + message = &s + response.Name, response.Details, response.YAML, err = client.GetPhase(id) + case configs.GetDocumentsBySelector: + id = request.ID + message = request.Message + response.Data, err = GetDocumentsBySelector(request.ID, *message) + case configs.GetTarget: + message = client.getTarget() + case configs.GetExecutorDoc: + id = request.ID + s := "rendered" + message = &s + response.Name, response.YAML, err = client.GetExecutorDoc(id) default: err = fmt.Errorf("Subcomponent %s not found", request.SubComponent) } @@ -64,6 +100,7 @@ func HandlePhaseRequest(user *string, request configs.WsMessage) configs.WsMessa response.Error = &e } else { response.Message = message + response.ID = id } return response @@ -80,6 +117,338 @@ func validateHelper(valid bool) *string { return &msg } +// GetExecutorDoc returns the title and YAML of the executor document for the specified phase +func (c *Client) GetExecutorDoc(id string) (string, string, error) { + helper, err := getHelper() + if err != nil { + return "", "", err + } + + phaseID := ifc.ID{} + + err = json.Unmarshal([]byte(id), &phaseID) + if err != nil { + return "", "", err + } + + ed, err := helper.ExecutorDoc(phaseID) + if err != nil { + return "", "", err + } + + title := ed.GetName() + bytes, err := ed.AsYAML() + if err != nil { + return "", "", err + } + + return title, base64.StdEncoding.EncodeToString(bytes), nil +} + +func (c *Client) getTarget() *string { + var s string + m, err := c.Config.CurrentContextManifest() + if err != nil { + s = "unknown" + return &s + } + + s = filepath.Join(m.TargetPath, m.SubPath) + return &s +} + +func (c *Client) getPhaseDetails(id ifc.ID) (string, error) { + helper, err := getHelper() + if err != nil { + return "", err + } + + pClient := phase.NewClient(helper) + + phase, err := pClient.PhaseByID(id) + if err != nil { + return "", err + } + + return phase.Details() +} + +func (c *Client) getYaml(id, message string) (string, string, error) { + switch message { + case "source": + name, yaml, err := c.getFileYaml(id) + return name, yaml, err + case "rendered": + name, yaml, err := c.getDocumentYaml(id) + return name, yaml, err + default: + return "", "", fmt.Errorf("'%s' unrecognized document type", message) + } +} + +func (c *Client) getDocumentYaml(id string) (string, string, error) { + doc, ok := docIndex[id] + if !ok { + return "", "", fmt.Errorf("document with ID '%s' not found", id) + } + title := doc.GetName() + bytes, err := doc.AsYAML() + if err != nil { + return "", "", err + } + + return title, base64.StdEncoding.EncodeToString(bytes), nil +} + +func (c *Client) getFileYaml(id string) (string, string, error) { + path, ok := fileIndex[id] + if !ok { + return "", "", fmt.Errorf("file with ID '%s' not found", id) + } + + ccm, err := c.Config.CurrentContextManifest() + if err != nil { + return "", "", err + } + + // this is making the assumption that the site definition + // will always found at: targetPath/subPath + sitePath := filepath.Join(ccm.TargetPath, ccm.SubPath) + + // TODO(mfuller): will this be true in treasuremap or + // other external repos? + manifestsDir := filepath.Join(sitePath, "..", "..") + + title, err := filepath.Rel(manifestsDir, path) + if err != nil { + return "", "", err + } + + file, err := os.Open(path) + if err != nil { + return "", "", err + } + + defer file.Close() + + bytes, err := ioutil.ReadAll(file) + if err != nil { + return "", "", err + } + + return title, base64.StdEncoding.EncodeToString(bytes), nil +} + +func (c *Client) writeYamlFile(id, yaml64 string) (string, string, error) { + path, ok := fileIndex[id] + if !ok { + return "", "", fmt.Errorf("ID %s not found", id) + } + + yaml, err := base64.StdEncoding.DecodeString(yaml64) + if err != nil { + return "", "", err + } + + err = ioutil.WriteFile(path, yaml, 0600) + if err != nil { + return "", "", err + } + + return c.getFileYaml(id) +} + +func getPhaseBundle(id ifc.ID) (document.Bundle, error) { + helper, err := getHelper() + if err != nil { + return nil, err + } + + pClient := phase.NewClient(helper) + + phase, err := pClient.PhaseByID(id) + if err != nil { + return nil, err + } + + docRoot, err := phase.DocumentRoot() + if err != nil { + // if phase has no doc entrypoint defined, just + // return nothing; otherwise, return the error + if strings.Contains(err.Error(), "defined") { + return nil, nil + } + return nil, err + } + + b, err := document.NewBundleByPath(docRoot) + if err != nil { + return nil, err + } + return b, nil +} + +// GetPhase returns the name, description, and doc bundle for specified phase +func (c *Client) GetPhase(id string) (string, string, string, error) { + phaseID := ifc.ID{} + + err := json.Unmarshal([]byte(id), &phaseID) + if err != nil { + return "", "", "", err + } + + title := phaseID.Name + + details, err := c.getPhaseDetails(phaseID) + if err != nil { + return "", "", "", err + } + + bundle, err := getPhaseBundle(phaseID) + if err != nil { + return "", "", "", err + } + + // only return title if phase has no bundle + if bundle == nil { + return title, details, "", nil + } + + var buf bytes.Buffer + err = bundle.Write(&buf) + if err != nil { + return "", "", "", err + } + + return title, details, base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +// SelectorParams structure to hold data for constructing a document Selector +type SelectorParams struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + GVK GVK `json:"gvk,omitempty"` + Kind string `json:"kind,omitempty"` + Label string `json:"label,omitempty"` + Annotation string `json:"annotation,omitempty"` +} + +// GVK small structure to hold group, version, kind for building a Selector +type GVK struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +// GetDocumentsBySelector returns a slice of KustomNodes representing all phase +// documents returned by applying the provided Selector +func GetDocumentsBySelector(id string, data string) ([]KustomNode, error) { + docIndex = map[string]document.Document{} + + selector, err := getSelector(data) + if err != nil { + return nil, err + } + + phaseID := ifc.ID{} + err = json.Unmarshal([]byte(id), &phaseID) + if err != nil { + return nil, err + } + + helper, err := getHelper() + if err != nil { + return nil, err + } + + pClient := phase.NewClient(helper) + + phase, err := pClient.PhaseByID(phaseID) + if err != nil { + return nil, err + } + + docRoot, err := phase.DocumentRoot() + if err != nil { + return nil, err + } + + bundle, err := document.NewBundleByPath(docRoot) + if err != nil { + return nil, err + } + + docs, err := bundle.Select(selector) + if err != nil { + return nil, err + } + + results := []KustomNode{} + + for _, doc := range docs { + // this is a workaround for a kustomize issue where cluster-scoped objects + // are included in matching results when a namespace selector is specified + // (https://github.com/kubernetes-sigs/kustomize/issues/2248) + if selector.Namespace != "" && selector.Namespace != doc.GetNamespace() { + continue + } + + id := uuid.New().String() + docIndex[id] = doc + + name := doc.GetNamespace() + if name == "" { + name = "[none]" + } + + results = append(results, KustomNode{ + ID: id, + Name: fmt.Sprintf("%s/%s/%s", + name, + doc.GetKind(), + doc.GetName(), + )}, + ) + } + + return results, nil +} + +func getSelector(data string) (document.Selector, error) { + params := SelectorParams{} + err := json.Unmarshal([]byte(data), ¶ms) + if err != nil { + return document.Selector{}, err + } + + s := document.NewSelector() + + // build selector based on what we were given + if params.Name != "" { + s = s.ByName(params.Name) + } + if params.Namespace != "" { + s = s.ByNamespace(params.Namespace) + } + if (GVK{}) != params.GVK { + s = s.ByGvk( + params.GVK.Group, + params.GVK.Version, + params.GVK.Kind, + ) + } + if params.Kind != "" { + s = s.ByKind(params.Kind) + } + if params.Label != "" { + s = s.ByLabel(params.Label) + } + if params.Annotation != "" { + s = s.ByAnnotation(params.Annotation) + } + return s, nil +} + // ValidatePhase validates the specified phase // (ifc.Phase.Validate isn't implemented yet, so this function // currently always returns "valid") diff --git a/pkg/ctl/processor.go b/pkg/ctl/processor.go index 8123211..b851aef 100644 --- a/pkg/ctl/processor.go +++ b/pkg/ctl/processor.go @@ -122,7 +122,7 @@ func sendEventMessage(sessionID, eventType, message string) { err := webservice.WebSocketSend(configs.WsMessage{ SessionID: sessionID, Type: configs.CTL, - Component: configs.Document, // probably will change to configs.Phase soon + Component: configs.Phase, SubComponent: configs.Run, Message: &m, })