diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b57d1dbef..39db868bf4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,18 +6,19 @@ version: 2 jobs: build: docker: - - image: circleci/node:8.11.3-stretch-browsers + - image: circleci/node:8.11.3-stretch-browsers working_directory: ~/repo steps: - - checkout - - run: yarn install - - run: yarn start format.check - - run: yarn start lint - - run: yarn start server.compile - - run: yarn start test - - run: yarn start e2e.fixtures - - run: - command: yarn start e2e.run - environment: - CYPRESS_RECORD_KEY: b8ec9ad7-505f-48bb-9990-e8d5627bac26 + - checkout + - run: yarn install + - run: yarn start format.check + - run: yarn start lint + - run: yarn start server.compile + - run: yarn start test + - run: yarn start e2e.fixtures + - run: + command: yarn start e2e.run + no_output_timeout: 20m + environment: + CYPRESS_RECORD_KEY: b8ec9ad7-505f-48bb-9990-e8d5627bac26 diff --git a/angular.json b/angular.json index 8686febb4d..3094c43afc 100644 --- a/angular.json +++ b/angular.json @@ -100,8 +100,7 @@ "root": "apps/angular-console-e2e/", "sourceRoot": "apps/angular-console-e2e/src", "projectType": "application", - "architect": { - } + "architect": {} }, "feature-install-node-js": { "root": "libs/feature-install-node-js", @@ -317,6 +316,25 @@ } } } + }, + "feature-action-bar": { + "root": "libs/feature-action-bar", + "sourceRoot": "libs/feature-action-bar/src", + "projectType": "library", + "prefix": "angular-console", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "libs/feature-action-bar/tsconfig.lib.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "cli": { @@ -327,5 +345,13 @@ "packageManager": "yarn", "defaultCollection": "@nrwl/schematics" }, + "schematics": { + "@nrwl/schematics:component": { + "styleext": "scss" + }, + "@nrwl/schematics:library": { + "unitTestRunner": "jest" + } + }, "defaultProject": "angular-console" } diff --git a/apps/angular-console-e2e/src/integration/extensions.spec.ts b/apps/angular-console-e2e/src/integration/extensions.spec.ts index ecc3d3243a..828a18c870 100644 --- a/apps/angular-console-e2e/src/integration/extensions.spec.ts +++ b/apps/angular-console-e2e/src/integration/extensions.spec.ts @@ -10,11 +10,14 @@ import { tasks, texts, waitForActionToComplete, - waitForAnimation + waitForAnimation, + whitelistGraphql } from './utils'; +import { clearRecentTask } from './tasks.utils'; describe('Extensions', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj-extensions')); goToExtensions(); @@ -57,8 +60,15 @@ describe('Extensions', () => { // check that the schematics added by angular material are available goToGenerate(); + cy.wait(300); // Needed to de-flake this test taskListHeaders($p => { expect(texts($p)[0]).to.equal('@angular/material'); }); }); + + after(() => { + cy.visit('/workspaces'); + openProject(projectPath('proj')); + clearRecentTask(); + }); }); diff --git a/apps/angular-console-e2e/src/integration/forms.spec.ts b/apps/angular-console-e2e/src/integration/forms.spec.ts index 40ccce0efd..a0f74153da 100644 --- a/apps/angular-console-e2e/src/integration/forms.spec.ts +++ b/apps/angular-console-e2e/src/integration/forms.spec.ts @@ -10,11 +10,13 @@ import { texts, toggleBoolean, uniqName, - waitForAutocomplete + waitForAutocomplete, + whitelistGraphql } from './utils'; describe('Forms', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj')); goToGenerate(); diff --git a/apps/angular-console-e2e/src/integration/generate.spec.ts b/apps/angular-console-e2e/src/integration/generate.spec.ts index 9a838e577a..f4c7acfe47 100644 --- a/apps/angular-console-e2e/src/integration/generate.spec.ts +++ b/apps/angular-console-e2e/src/integration/generate.spec.ts @@ -9,11 +9,14 @@ import { taskListHeaders, tasks, texts, - uniqName + uniqName, + whitelistGraphql } from './utils'; +import { clearRecentTask } from './tasks.utils'; describe('Generate', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj')); goToGenerate(); @@ -79,4 +82,10 @@ describe('Generate', () => { expect(texts($p)[0]).to.equal('@schematics/angular'); }); }); + + after(() => { + cy.visit('/workspaces'); + openProject(projectPath('proj')); + clearRecentTask(); + }); }); diff --git a/apps/angular-console-e2e/src/integration/no_node_modules.spec.ts b/apps/angular-console-e2e/src/integration/no_node_modules.spec.ts index 0a32f017e3..df1474a27e 100644 --- a/apps/angular-console-e2e/src/integration/no_node_modules.spec.ts +++ b/apps/angular-console-e2e/src/integration/no_node_modules.spec.ts @@ -7,11 +7,14 @@ import { projectNames, projectPath, taskListHeaders, - texts + texts, + whitelistGraphql } from './utils'; +import { toggleRecentTasksExpansion, clearAllRecentTasks } from './tasks.utils'; describe('Project without node modules', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj-no-node-modules')); }); diff --git a/apps/angular-console-e2e/src/integration/projects.spec.ts b/apps/angular-console-e2e/src/integration/projects.spec.ts index a033fd32d1..1a9eea1978 100644 --- a/apps/angular-console-e2e/src/integration/projects.spec.ts +++ b/apps/angular-console-e2e/src/integration/projects.spec.ts @@ -3,11 +3,13 @@ import { projectNames, projectPath, texts, - waitForAnimation + waitForAnimation, + whitelistGraphql } from './utils'; describe('Projects', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj')); cy.get('div.title').contains('Projects'); diff --git a/apps/angular-console-e2e/src/integration/tasks.spec.ts b/apps/angular-console-e2e/src/integration/tasks.spec.ts index a6bd77cc6a..d4908526da 100644 --- a/apps/angular-console-e2e/src/integration/tasks.spec.ts +++ b/apps/angular-console-e2e/src/integration/tasks.spec.ts @@ -10,11 +10,21 @@ import { taskListHeaders, tasks, texts, - waitForActionToComplete + waitForActionToComplete, + whitelistGraphql } from './utils'; +import { + checkMultipleRecentTasks, + checkSingleRecentTask, + CommandStatus, + toggleRecentTasksExpansion, + checkActionBarHidden, + clearAllRecentTasks +} from './tasks.utils'; describe('Tasks', () => { beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); openProject(projectPath('proj')); goToTasks(); @@ -76,6 +86,7 @@ describe('Tasks', () => { waitForActionToComplete(); checkFileExists(`dist/proj/main.js`); + checkActionBarHidden(); goBack(); @@ -85,7 +96,12 @@ describe('Tasks', () => { }); }); - it('cancels a task when navigating away', () => { + it('show the recent tasks bar after navigating away', () => { + checkSingleRecentTask({ + command: 'ng build proj', + status: CommandStatus.SUCCESSFUL + }); + clickOnTask('proj', 'test'); cy.get('div.context-title').contains('ng test proj'); @@ -95,12 +111,33 @@ describe('Tasks', () => { .contains('Run') .click(); + checkActionBarHidden(); + cy.wait(100); goBack(); cy.get('div.title').contains('Run Tasks'); - checkMessage('Command has been canceled'); + + checkMultipleRecentTasks({ + numTasks: 2, + isExpanded: false + }); + + toggleRecentTasksExpansion(); + + checkMultipleRecentTasks({ + numTasks: 2, + isExpanded: true, + tasks: [ + { command: 'ng test proj', status: CommandStatus.IN_PROGRESS }, + { command: 'ng build proj', status: CommandStatus.SUCCESSFUL } + ] + }); + + clearAllRecentTasks(); + + checkActionBarHidden(); }); it('runs an npm script', () => { diff --git a/apps/angular-console-e2e/src/integration/tasks.utils.ts b/apps/angular-console-e2e/src/integration/tasks.utils.ts new file mode 100644 index 0000000000..f200d7b79e --- /dev/null +++ b/apps/angular-console-e2e/src/integration/tasks.utils.ts @@ -0,0 +1,94 @@ +interface Task { + command: string; + status: CommandStatus; +} + +export enum CommandStatus { + SUCCESSFUL = 'successful', + FAILED = 'failed', + IN_PROGRESS = 'in-progress', + TERMINATED = 'terminated' +} + +export function checkActionBarHidden() { + cy.get('angular-console-action-bar .action-bar').should($el => { + expect($el).to.have.css('height', '0px'); + }); + + cy.get('angular-console-action-bar mat-list').should($el => { + expect($el).to.have.css('height', '0px'); + }); +} + +export function checkSingleRecentTask(task: Task) { + cy.get('angular-console-action-bar mat-list-item').should(tasks => { + expect(tasks.length).to.equal(1); + expect(tasks).visible; + + expect( + tasks + .find('.command') + .get(0) + .textContent.trim() + ).to.equal(task.command); + + expect(tasks.find(`.task-avatar.${task.status}`)).visible; + }); +} + +export function checkMultipleRecentTasks(options: { + tasks?: Task[]; + isExpanded: boolean; + numTasks: number; +}) { + cy.get('angular-console-action-bar').should(actionBar => { + expect( + actionBar + .find('.num-tasks') + .text() + .trim() + ).to.equal(`${options.numTasks} Tasks`); + + if (!options.isExpanded) { + expect(actionBar.find('.remove-all-tasks-button').length).to.equal(0); + } else { + expect(actionBar.find('.remove-all-tasks-button').length).to.equal(1); + } + + const taskElements = actionBar.get(0).querySelectorAll('mat-list-item'); + + expect(taskElements.length).to.equal(options.numTasks); + expect(taskElements).visible; + + if (options.tasks) { + options.tasks.forEach((task, index) => { + const taskElement = taskElements[index]; + expect( + taskElement.querySelector('.command').textContent.trim() + ).to.equal(task.command); + + expect(taskElement.querySelector(`.task-avatar.${task.status}`)).not.to + .be.null; + }); + } + }); +} + +export function toggleRecentTasksExpansion() { + cy.get('angular-console-action-bar .action-bar').click({ force: true }); + cy.wait(500); +} + +export function clearAllRecentTasks() { + cy.get( + 'angular-console-action-bar .action-bar .remove-all-tasks-button' + ).click({ force: true }); + cy.wait(500); +} + +export function clearRecentTask() { + cy.get('angular-console-action-bar .remove-task-button').click({ + force: true + }); + cy.wait(500); +} diff --git a/apps/angular-console-e2e/src/integration/utils.ts b/apps/angular-console-e2e/src/integration/utils.ts index 5511e751c7..d38d29f3c4 100644 --- a/apps/angular-console-e2e/src/integration/utils.ts +++ b/apps/angular-console-e2e/src/integration/utils.ts @@ -1,5 +1,21 @@ import * as path from 'path'; +export function whitelistGraphql() { + cy.server({ + whitelist: xhr => { + if (xhr.url.indexOf('graphql') !== -1) { + return true; + } + // this function receives the xhr object in question and + // will whitelist if it's a GET that appears to be a static resource + return ( + xhr.method === 'GET' && + /\.(jsx?|html|css|svg|png|jpg)(\?.*)?$/.test(xhr.url) + ); + } + }); +} + export function clickOnFieldGroup(group: string) { cy.get('mat-expansion-panel-header') .contains(group) @@ -78,24 +94,24 @@ export function projectNames(callback: (s: any) => void) { } export function goToGenerate() { - cy.get('button#go-to-generate').click(); + cy.get('button#go-to-generate').click({ force: true }); waitForAnimation(); } export function goToExtensions() { - cy.get('button#go-to-extensions').click(); + cy.get('button#go-to-extensions').click({ force: true }); waitForAnimation(); } export function goToTasks() { - cy.get('button#go-to-tasks').click(); + cy.get('button#go-to-tasks').click({ force: true }); waitForAnimation(); } export function taskListHeaders(callback: (s: any) => void) { cy.get('mat-nav-list.task-list').within(() => { cy.root() - .find('h3.mat-subheader') + .find('h3.mat-subheader', { timeout: 5000 }) .should(callback); }); } diff --git a/apps/angular-console-e2e/src/integration/workspaces.spec.ts b/apps/angular-console-e2e/src/integration/workspaces.spec.ts index cb28eb66a0..fdaeea427b 100644 --- a/apps/angular-console-e2e/src/integration/workspaces.spec.ts +++ b/apps/angular-console-e2e/src/integration/workspaces.spec.ts @@ -3,13 +3,15 @@ import { expandFolder, selectFolder, uniqName, - waitForNgNew + waitForNgNew, + whitelistGraphql } from './utils'; describe('Workspaces', () => { const name = uniqName('workspace'); beforeEach(() => { + whitelistGraphql(); cy.visit('/workspaces'); }); diff --git a/apps/angular-console/src/app/app.component.css b/apps/angular-console/src/app/app.component.css index 5a1b4f658e..79ee6035c3 100644 --- a/apps/angular-console/src/app/app.component.css +++ b/apps/angular-console/src/app/app.component.css @@ -4,6 +4,10 @@ overflow: hidden; } +.content-container { + position: relative; +} + mat-sidenav-container { height: 100%; } diff --git a/apps/angular-console/src/app/app.component.html b/apps/angular-console/src/app/app.component.html index 4376f7953a..73fdf2459b 100644 --- a/apps/angular-console/src/app/app.component.html +++ b/apps/angular-console/src/app/app.component.html @@ -12,14 +12,14 @@ -
+
-
+
diff --git a/apps/angular-console/src/app/app.module.ts b/apps/angular-console/src/app/app.module.ts index ba80e01606..2961e92d6c 100644 --- a/apps/angular-console/src/app/app.module.ts +++ b/apps/angular-console/src/app/app.module.ts @@ -5,7 +5,6 @@ import { import { UiModule } from '@angular-console/ui'; import { AnalyticsCollector, - CancelCommandGuard, IsNodeJsInstalledGuard, Messenger } from '@angular-console/utils'; @@ -31,7 +30,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import { onError } from 'apollo-link-error'; import { AppComponent } from './app.component'; -import { BackgroundTasksComponent } from './background-tasks.component'; +import { FeatureActionBarModule } from '@angular-console/feature-action-bar'; export function initApollo( analytics: AnalyticsCollector, @@ -70,11 +69,9 @@ export function initApollo( } @NgModule({ - declarations: [ - AppComponent, - BackgroundTasksComponent // delete it - ], + declarations: [AppComponent], imports: [ + FeatureActionBarModule, MatSidenavModule, MatListModule, MatIconModule, @@ -93,7 +90,7 @@ export function initApollo( { path: '', children: workspaceRoutes, - canActivateChild: [IsNodeJsInstalledGuard, CancelCommandGuard] + canActivateChild: [IsNodeJsInstalledGuard] }, { path: 'install-nodejs', @@ -110,7 +107,6 @@ export function initApollo( ], providers: [ IsNodeJsInstalledGuard, - CancelCommandGuard, AnalyticsCollector, { provide: APOLLO_OPTIONS, diff --git a/apps/angular-console/src/app/background-tasks.component.ts b/apps/angular-console/src/app/background-tasks.component.ts deleted file mode 100644 index bb483ba6bd..0000000000 --- a/apps/angular-console/src/app/background-tasks.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component } from '@angular/core'; -import { CommandRunner, Settings } from '@angular-console/utils'; -import { first } from 'rxjs/operators'; - -/** - * Delete this component once background tasks functionality is fully implemented - */ -@Component({ - selector: 'angular-console-background-tasks', - template: ` -
-

background tasks

-
-
id: {{ task.id }}
-
status: {{ task.status }}
-
workspace: {{ task.workspace }}
-
command: {{ task.command }}
-
- - - - -
-
-
- ` -}) -export class BackgroundTasksComponent { - allCommands = this.runner.listAllCommands(); - - constructor(public settings: Settings, public runner: CommandRunner) {} - - stop(id: string) { - this.runner.stopCommand(id); - } - - restart(id: string) { - this.runner.restartCommand(id); - } - - remove(id: string) { - this.runner.removeCommand(id); - } - - open(id: string) { - this.runner - .getCommand(id) - .pipe(first()) - .subscribe(v => { - console.log(JSON.stringify(v, null, 2)); - }); - } -} diff --git a/apps/angular-console/src/assets/roboto-mono.woff2 b/apps/angular-console/src/assets/roboto-mono.woff2 new file mode 100644 index 0000000000..31110fe9ed Binary files /dev/null and b/apps/angular-console/src/assets/roboto-mono.woff2 differ diff --git a/apps/angular-console/src/assets/terminal.svg b/apps/angular-console/src/assets/terminal.svg new file mode 100644 index 0000000000..9fde85b172 --- /dev/null +++ b/apps/angular-console/src/assets/terminal.svg @@ -0,0 +1,6 @@ + diff --git a/apps/angular-console/src/assets/xterm.css b/apps/angular-console/src/assets/xterm.css index d5825ed6ea..2df26cfceb 100644 --- a/apps/angular-console/src/assets/xterm.css +++ b/apps/angular-console/src/assets/xterm.css @@ -36,7 +36,7 @@ */ .xterm { - font-family: courier-new, courier, monospace; + font-family: 'Roboto Mono', monospace; font-feature-settings: 'liga' 0; position: relative; user-select: none; diff --git a/apps/angular-console/src/styles.scss b/apps/angular-console/src/styles.scss index fd89a8b92b..90a0e8ca4e 100644 --- a/apps/angular-console/src/styles.scss +++ b/apps/angular-console/src/styles.scss @@ -65,6 +65,18 @@ body { U+FEFF, U+FFFD; } +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + src: local('Roboto Mono'), local('RobotoMono-Regular'), + url(assets/roboto-mono.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} + /* Material Icons Extended */ @font-face { font-family: 'Material Icons Extended'; diff --git a/libs/feature-action-bar/src/index.ts b/libs/feature-action-bar/src/index.ts new file mode 100644 index 0000000000..2285555f62 --- /dev/null +++ b/libs/feature-action-bar/src/index.ts @@ -0,0 +1 @@ +export * from './lib/feature-action-bar.module'; diff --git a/libs/feature-action-bar/src/lib/action-bar.component.html b/libs/feature-action-bar/src/lib/action-bar.component.html new file mode 100644 index 0000000000..6bfc14644e --- /dev/null +++ b/libs/feature-action-bar/src/lib/action-bar.component.html @@ -0,0 +1,81 @@ +
+ +
+ + + +
+ + + {{ actions.length }} Tasks + + +
+
+ + + + + + + + + + done + + + error + + + clear + + + + + + + + + + + + +

+ {{ action.command }} +

+ +

+ + + {{ action.workspace }} +

+ + +
+ + +
+ +
+ + +
+
+
diff --git a/libs/feature-action-bar/src/lib/action-bar.component.scss b/libs/feature-action-bar/src/lib/action-bar.component.scss new file mode 100644 index 0000000000..447e432a01 --- /dev/null +++ b/libs/feature-action-bar/src/lib/action-bar.component.scss @@ -0,0 +1,182 @@ +.action-bar-spacer { + height: 64px; +} + +.action-bar-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + + box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12); + bottom: 0; + left: 0; + right: 0; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + position: absolute; + overflow: hidden; + background: white; + z-index: 10; + max-height: calc(100vh - 24px); + overflow-y: hidden; +} + +.action-bar { + overflow: hidden; + width: auto; + position: relative; + cursor: pointer; + + span { + user-select: none; + } +} + +.terminal-container { + margin: 0px 16px; + margin-bottom: 16px; + background: black; + padding: 8px; + border-radius: 8px; + box-sizing: border-box; + + ui-terminal { + display: block; + height: 100%; + } +} + +mat-list { + padding: 0; + + mat-list-item { + height: 64px !important; + position: relative; + cursor: pointer; + z-index: 2; + + .command { + font-family: 'Roboto Mono', monospace; + padding-right: 16px !important; + font-size: 16px; + color: #000000de; + line-height: 20px; + } + + .replay-button mat-icon { + position: relative; + top: 2px !important; + } + + .task-avatar { + line-height: 40px; + + text-align: center; + color: white; + position: relative; + background: rgba(0, 0, 0, 0.54); + + &:hover:not(.freshly-toggled) { + background: rgba(0, 0, 0, 0.87); + } + + .process-action mat-icon { + position: relative; + top: -1px; + } + + &.in-progress { + background: none; + + .process-action mat-icon { + top: 1px; + } + } + + .task-status { + opacity: 1; + cursor: default; + } + + .task-status, + .process-action { + transition: opacity 0.3s ease-in-out; + } + + &.freshly-toggled { + .task-status, + .process-action { + transition: none; + } + + .process-action { + pointer-events: none; + } + } + + &:hover:not(.freshly-toggled) { + .task-status { + opacity: 0; + } + + .process-action { + opacity: 1; + } + } + + .process-action { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0; + z-index: 11; + color: #4275c6; + } + + mat-icon { + height: 28px; + width: 28px; + font-size: 28px; + color: white; + } + } + + .workspace-indicator { + border-radius: 50%; + height: 12px; + width: 12px; + min-height: 12px; + min-width: 12px; + display: inline-block; + margin-right: 6px; + position: relative; + top: 1px; + } + + .task-action { + &:hover { + mat-icon { + color: rgba(0, 0, 0, 0.87); + } + } + mat-icon { + transition: color 0.15s ease-in-out; + color: rgba(0, 0, 0, 0.54); + } + } + + .workspace-name { + color: #00000099; + } + + .second-line { + padding-top: 6px !important; + font-size: 14px; + line-height: 16px; + } + } +} diff --git a/libs/feature-action-bar/src/lib/action-bar.component.ts b/libs/feature-action-bar/src/lib/action-bar.component.ts new file mode 100644 index 0000000000..430f8f1d56 --- /dev/null +++ b/libs/feature-action-bar/src/lib/action-bar.component.ts @@ -0,0 +1,167 @@ +import { + state, + style, + trigger, + transition, + animate +} from '@angular/animations'; +import { Component, ViewChildren, QueryList } from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { + map, + shareReplay, + tap, + distinctUntilChanged, + startWith +} from 'rxjs/operators'; +import { + CommandRunner, + CommandResponse, + CommandStatus +} from '@angular-console/utils'; +import { ContextualActionBarService } from '@nrwl/angular-console-enterprise-frontend'; +import { TerminalComponent } from '@angular-console/ui'; + +const TERMINAL_PADDING = 44; +const COMMAND_HEIGHT = 64; + +@Component({ + selector: 'angular-console-action-bar', + templateUrl: './action-bar.component.html', + styleUrls: ['./action-bar.component.scss'], + animations: [ + trigger('growShrink', [ + state('void', style({ height: 0, opacity: 0 })), + state('contract', style({ height: 0, opacity: 0 })), + state('*', style({ height: '*', opacity: 1 })), + transition(`contract <=> *`, animate(`250ms ease-in-out`)), + transition(`:enter`, animate(`250ms ease-in-out`)), + transition(`:leave`, animate(`250ms ease-in-out`)) + ]), + trigger('growShrinkTerminal', [ + state( + 'void', + style({ + height: 0, + opacity: 0, + 'margin-bottom': '0', + 'padding-top': '0', + 'padding-bottom': '0' + }) + ), + state( + '*', + style({ + height: '{{terminalHeight}}', // use interpolation + opacity: 1, + 'margin-bottom': '16px', + 'padding-top': '8px', + 'padding-bottom': '8px' + }), + { params: { terminalHeight: '0' } } + ), + transition(`* <=> *`, animate(`250ms ease-in-out`)) + ]) + ] +}) +export class ActionBarComponent { + @ViewChildren(TerminalComponent) + activeTerminals?: QueryList; + + // For use within the action bar's template. + CommandStatus = CommandStatus; + + // Whether to show list of actions If there are multiple actions, + actionsExpanded = new BehaviorSubject(false); + + // The action showing its terminal output if one exists. + expandedAction?: { + id: string; + command: Observable; + }; + + // The calculated height of the expanded action's terminal. + terminalHeight: string; + + // The user's list of recently run commands. + commands$ = this.commandRunner.listAllCommands().pipe(shareReplay()); + + showActionBar$ = combineLatest( + this.commands$, + this.contextualActionBarService.contextualActions$.pipe(startWith(null)) + ).pipe( + map(([commands, contextualActions]) => { + if (contextualActions) { + return false; + } + return commands.length > 0; + }), + tap(show => { + if (!show) { + this.actionsExpanded.next(false); + } + }), + shareReplay() + ); + + showActionToolbar$ = combineLatest(this.showActionBar$, this.commands$).pipe( + map(([showActionBar, commands]) => { + return Boolean(showActionBar && commands.length > 1); + }) + ); + + showActionList$ = combineLatest( + this.showActionBar$, + this.commands$, + this.actionsExpanded + ).pipe( + map(([showActionBar, commands, actionsExpanded]) => { + return Boolean( + showActionBar && (actionsExpanded || commands.length === 1) + ); + }) + ); + + // Show/hide a particular items terminal output. + toggleItemExpansion(actionId: string) { + if (this.expandedAction && actionId === this.expandedAction.id) { + this.expandedAction = undefined; + } else { + this.expandedAction = { + id: actionId, + command: this.commandRunner.getCommand(actionId).pipe(shareReplay()) + }; + } + } + + trackByCommandId(_: number, command: CommandResponse) { + return command.id; + } + + constructor( + readonly commandRunner: CommandRunner, + private readonly contextualActionBarService: ContextualActionBarService + ) { + this.commands$ + .pipe( + map(commands => commands.length), + distinctUntilChanged(), + map(numCommands => { + const actionBarHeight = numCommands > 1 ? COMMAND_HEIGHT : 0; + return `calc(100vh - ${TERMINAL_PADDING + + actionBarHeight + + COMMAND_HEIGHT * numCommands}px)`; + }), + tap(() => { + setTimeout(() => { + if (this.activeTerminals && this.activeTerminals.first) { + this.activeTerminals.first.resizeTerminal(); + } + }, 250); + }) + ) + .subscribe(height => { + this.terminalHeight = height; + }); + } +} diff --git a/libs/feature-action-bar/src/lib/feature-action-bar.module.ts b/libs/feature-action-bar/src/lib/feature-action-bar.module.ts new file mode 100644 index 0000000000..1a4f04ee9f --- /dev/null +++ b/libs/feature-action-bar/src/lib/feature-action-bar.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ActionBarComponent } from './action-bar.component'; +import { UiModule } from '@angular-console/ui'; + +@NgModule({ + imports: [UiModule], + exports: [ActionBarComponent], + declarations: [ActionBarComponent] +}) +export class FeatureActionBarModule {} diff --git a/libs/feature-action-bar/tsconfig.lib.json b/libs/feature-action-bar/tsconfig.lib.json new file mode 100644 index 0000000000..4f3eb80a8d --- /dev/null +++ b/libs/feature-action-bar/tsconfig.lib.json @@ -0,0 +1,32 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/libs/feature-action-bar", + "target": "es2015", + "module": "es2015", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "types": [], + "lib": [ + "dom", + "es2015" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/libs/feature-action-bar/tslint.json b/libs/feature-action-bar/tslint.json new file mode 100644 index 0000000000..43c8151e56 --- /dev/null +++ b/libs/feature-action-bar/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "angular-console", + "camelCase" + ], + "component-selector": [ + true, + "element", + "angular-console", + "kebab-case" + ] + } +} diff --git a/libs/feature-extensions/src/lib/extension/extension.component.html b/libs/feature-extensions/src/lib/extension/extension.component.html index 79067c2b68..7471e34980 100644 --- a/libs/feature-extensions/src/lib/extension/extension.component.html +++ b/libs/feature-extensions/src/lib/extension/extension.component.html @@ -7,5 +7,5 @@ {{ extension.description }}
- + diff --git a/libs/feature-generate/src/lib/schematic/schematic.component.html b/libs/feature-generate/src/lib/schematic/schematic.component.html index d04314130a..098b290cc5 100644 --- a/libs/feature-generate/src/lib/schematic/schematic.component.html +++ b/libs/feature-generate/src/lib/schematic/schematic.component.html @@ -1,5 +1,5 @@ - + diff --git a/libs/feature-generate/src/lib/schematic/schematic.component.ts b/libs/feature-generate/src/lib/schematic/schematic.component.ts index 6e95cb6575..4e42ce2a5d 100644 --- a/libs/feature-generate/src/lib/schematic/schematic.component.ts +++ b/libs/feature-generate/src/lib/schematic/schematic.component.ts @@ -167,7 +167,7 @@ export class SchematicComponent implements OnInit { this.out.reset(); if (!c.valid) { // cannot use change detection because the operation isn't idempotent - this.out.input = 'Command is missing required fields'; + this.out.out = 'Command is missing required fields'; return of(); } diff --git a/libs/feature-install-node-js/src/lib/install-node-js.component.ts b/libs/feature-install-node-js/src/lib/install-node-js.component.ts index 1bd21004a4..8ae0c266c9 100644 --- a/libs/feature-install-node-js/src/lib/install-node-js.component.ts +++ b/libs/feature-install-node-js/src/lib/install-node-js.component.ts @@ -37,11 +37,7 @@ export class InstallNodeJsComponent { ` }) .pipe( - map( - (response: { data: { installNodeJs: any } }) => - response.data.installNodeJs - ), - switchMap(status => { + switchMap(() => { return this.apollo .watchQuery({ pollInterval: NODE_JS_INSTALL_POLLING, @@ -58,10 +54,7 @@ export class InstallNodeJsComponent { ` }) .valueChanges.pipe( - map( - (result: { data: { installNodeJsStatus: any } }) => - result.data.installNodeJsStatus - ) + map((result: any) => result.data.installNodeJsStatus) ); }), tap(({ success, error }: any) => { diff --git a/libs/feature-run/src/lib/npmscript/npmscript.component.html b/libs/feature-run/src/lib/npmscript/npmscript.component.html index 4b3fb7418e..5071c7b2a9 100644 --- a/libs/feature-run/src/lib/npmscript/npmscript.component.html +++ b/libs/feature-run/src/lib/npmscript/npmscript.component.html @@ -1,5 +1,5 @@ - + diff --git a/libs/feature-run/src/lib/target/target.component.html b/libs/feature-run/src/lib/target/target.component.html index 682565f29a..b424be10e3 100644 --- a/libs/feature-run/src/lib/target/target.component.html +++ b/libs/feature-run/src/lib/target/target.component.html @@ -1,5 +1,5 @@ - + diff --git a/libs/feature-workspaces/src/lib/new-workspace/new-workspace-dialog.component.ts b/libs/feature-workspaces/src/lib/new-workspace/new-workspace-dialog.component.ts index 415d7ce2c9..8a8e0028e7 100644 --- a/libs/feature-workspaces/src/lib/new-workspace/new-workspace-dialog.component.ts +++ b/libs/feature-workspaces/src/lib/new-workspace/new-workspace-dialog.component.ts @@ -14,7 +14,7 @@ export interface NgNewInvocation { @Component({ selector: 'angular-console-new-workspace-dialog', template: ` - + ` }) export class NewWorkspaceDialogComponent { @@ -36,7 +36,7 @@ export class NewWorkspaceDialogComponent { ) .pipe( tap(command => { - if (command.status === 'success') { + if (command.status === 'successful') { this.dialogRef.close(); this.router.navigate([ '/workspace', diff --git a/libs/feature-workspaces/src/lib/new-workspace/new-workspace.component.ts b/libs/feature-workspaces/src/lib/new-workspace/new-workspace.component.ts index 134d2c6f00..39b0f30883 100644 --- a/libs/feature-workspaces/src/lib/new-workspace/new-workspace.component.ts +++ b/libs/feature-workspaces/src/lib/new-workspace/new-workspace.component.ts @@ -18,7 +18,13 @@ import { MatDialog, MatExpansionPanel } from '@angular/material'; import { ContextualActionBarService } from '@nrwl/angular-console-enterprise-frontend'; import { Apollo } from 'apollo-angular'; import gql from 'graphql-tag'; -import { of, BehaviorSubject, Observable, Subject } from 'rxjs'; +import { + BehaviorSubject, + Observable, + Subject, + OperatorFunction, + of +} from 'rxjs'; import { filter, map, @@ -120,7 +126,10 @@ export class NewWorkspaceComponent implements OnInit { this.ngNewForm$ .pipe( - filter(form => Boolean(form)), + filter(form => Boolean(form)) as OperatorFunction< + FormGroup | null, + FormGroup + >, switchMap((form: FormGroup) => form.statusChanges) ) .subscribe((formStatus: any) => { diff --git a/libs/feature-workspaces/src/lib/workspace/workspace.component.html b/libs/feature-workspaces/src/lib/workspace/workspace.component.html index 0b0c20e865..53cfdb0992 100644 --- a/libs/feature-workspaces/src/lib/workspace/workspace.component.html +++ b/libs/feature-workspaces/src/lib/workspace/workspace.component.html @@ -4,7 +4,10 @@
diff --git a/libs/feature-workspaces/src/lib/workspace/workspace.component.scss b/libs/feature-workspaces/src/lib/workspace/workspace.component.scss index 6efb1ed174..2c2f6d929d 100644 --- a/libs/feature-workspaces/src/lib/workspace/workspace.component.scss +++ b/libs/feature-workspaces/src/lib/workspace/workspace.component.scss @@ -1,6 +1,5 @@ angular-console-workspace { display: block; - height: calc(100vh - 64px); overflow: hidden; .content { diff --git a/libs/feature-workspaces/src/lib/workspace/workspace.component.ts b/libs/feature-workspaces/src/lib/workspace/workspace.component.ts index 4e25c91dc1..8e456194a7 100644 --- a/libs/feature-workspaces/src/lib/workspace/workspace.component.ts +++ b/libs/feature-workspaces/src/lib/workspace/workspace.component.ts @@ -31,7 +31,8 @@ import { } from '@nrwl/angular-console-enterprise-frontend'; interface Route { - icon: string; + icon?: string; + svgIcon?: string; url: string; title: string; } @@ -77,7 +78,7 @@ export class WorkspaceComponent implements OnDestroy { readonly routes: Array = [ { icon: 'view_list', url: 'projects', title: 'Projects' }, { icon: 'code', url: 'generate', title: 'Generate Code' }, - { icon: 'play_arrow', url: 'tasks', title: 'Run Tasks' }, + { svgIcon: 'terminal', url: 'tasks', title: 'Run Tasks' }, { icon: 'extension', url: 'extensions', diff --git a/libs/ui/src/lib/contextual-action-bar/contextual-action-bar.component.ts b/libs/ui/src/lib/contextual-action-bar/contextual-action-bar.component.ts index 3c6bfff399..a8ec0d9c22 100644 --- a/libs/ui/src/lib/contextual-action-bar/contextual-action-bar.component.ts +++ b/libs/ui/src/lib/contextual-action-bar/contextual-action-bar.component.ts @@ -13,7 +13,6 @@ import { Output } from '@angular/core'; import { - AuthService, ContextualActionBarService, ContextualTab } from '@nrwl/angular-console-enterprise-frontend'; @@ -43,18 +42,9 @@ export class ContextualActionBarComponent { constructor( readonly contextualActionBarService: ContextualActionBarService, readonly commandRunner: CommandRunner, - readonly messenger: Messenger, - readonly authService: AuthService + readonly messenger: Messenger ) {} - login() { - this.authService.auth(); - } - - logout() { - this.authService.unauth(); - } - trackByName(_: number, tab: ContextualTab) { return tab.name; } diff --git a/libs/ui/src/lib/task-runner/task-runner.component.html b/libs/ui/src/lib/task-runner/task-runner.component.html index 9bce2e4a73..aedbdf85e4 100644 --- a/libs/ui/src/lib/task-runner/task-runner.component.html +++ b/libs/ui/src/lib/task-runner/task-runner.component.html @@ -4,14 +4,7 @@
-
- - - - - - - +
{{ terminalWindowTitle }}
diff --git a/libs/ui/src/lib/terminal/terminal.component.scss b/libs/ui/src/lib/terminal/terminal.component.scss index c3482271d4..022433335c 100644 --- a/libs/ui/src/lib/terminal/terminal.component.scss +++ b/libs/ui/src/lib/terminal/terminal.component.scss @@ -6,7 +6,7 @@ .terminal-container { overflow: hidden; max-width: 100%; - height: calc(100% - 44px); + height: 100%; } .command { diff --git a/libs/ui/src/lib/terminal/terminal.component.ts b/libs/ui/src/lib/terminal/terminal.component.ts index 2a03a58b3f..ca256f6b30 100644 --- a/libs/ui/src/lib/terminal/terminal.component.ts +++ b/libs/ui/src/lib/terminal/terminal.component.ts @@ -40,7 +40,7 @@ export class TerminalComponent implements AfterViewInit, OnDestroy { @Input() command: string; @Input() - set input(s: string) { + set outChunk(s: string) { if (!s) { return; } @@ -48,6 +48,12 @@ export class TerminalComponent implements AfterViewInit, OnDestroy { this.writeOutput(s); } + @Input() + set out(s: string) { + this.output = s; + this.writeOutput(s); + } + ngAfterViewInit(): void { this.term.open(this.code.nativeElement); this.resizeTerminal(); diff --git a/libs/ui/src/lib/ui.module.ts b/libs/ui/src/lib/ui.module.ts index 61a97613de..ab21e430ba 100644 --- a/libs/ui/src/lib/ui.module.ts +++ b/libs/ui/src/lib/ui.module.ts @@ -24,7 +24,8 @@ import { MatToolbarModule, MatTooltipModule, MatTreeModule, - MatProgressBarModule + MatProgressBarModule, + MatProgressSpinnerModule } from '@angular/material'; import { DomSanitizer } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; @@ -39,6 +40,7 @@ import { TaskSelectorComponent } from './task-selector/task-selector.component'; import { TerminalComponent } from './terminal/terminal.component'; const IMPORTS = [ + MatProgressSpinnerModule, MatProgressBarModule, MatMenuModule, CdkTreeModule, @@ -121,6 +123,7 @@ export class UiModule { this.addIcon('vscode', 'vscode.svg'); this.addIcon('webstorm', 'webstorm.svg'); this.addIcon('intellij', 'intellij.svg'); + this.addIcon('terminal', 'terminal.svg'); } private addIcon(name: string, url: string) { diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts index da70935d32..5df3437f2e 100644 --- a/libs/utils/src/index.ts +++ b/libs/utils/src/index.ts @@ -8,4 +8,3 @@ export * from './lib/serializer.service'; export * from './lib/settings.service'; export * from './lib/is-node-js-installed.guard'; export * from './lib/polling-constants'; -export * from './lib/cancel-command.guard'; diff --git a/libs/utils/src/lib/cancel-command.guard.ts b/libs/utils/src/lib/cancel-command.guard.ts deleted file mode 100644 index 66644a7068..0000000000 --- a/libs/utils/src/lib/cancel-command.guard.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CanActivateChild } from '@angular/router'; -import { CommandRunner } from './command-runner.service'; -import { Messenger } from './messenger.service'; -import { Injectable } from '@angular/core'; -import { Settings } from './settings.service'; - -@Injectable() -export class CancelCommandGuard implements CanActivateChild { - constructor( - readonly messenger: Messenger, - readonly commandRunner: CommandRunner, - readonly settings: Settings - ) {} - - canActivateChild(): boolean { - if (this.settings.showBackgroundTasks()) { - return true; - } - if (this.commandRunner.activeCommand$.value) { - this.commandRunner.stopActiveCommand(); - this.messenger.notify('Command has been canceled'); - } - return true; - } -} diff --git a/libs/utils/src/lib/command-runner.service.ts b/libs/utils/src/lib/command-runner.service.ts index 01e0d09105..7cd8fe0d09 100644 --- a/libs/utils/src/lib/command-runner.service.ts +++ b/libs/utils/src/lib/command-runner.service.ts @@ -5,9 +5,17 @@ import gql from 'graphql-tag'; import { BehaviorSubject, interval, Observable, of } from 'rxjs'; import { concatMap, map, takeWhile } from 'rxjs/operators'; import { COMMANDS_POLLING } from './polling-constants'; +import { ContextualActionBarService } from '@nrwl/angular-console-enterprise-frontend'; + +export enum CommandStatus { + SUCCESSFUL = 'successful', + FAILED = 'failed', + IN_PROGRESS = 'in-progress', + TERMINATED = 'terminated' +} export interface IncrementalCommandOutput { - status: 'success' | 'failure' | 'inprogress' | 'terminated'; + status: CommandStatus; outChunk: string; detailedStatus: any; } @@ -18,7 +26,7 @@ export interface CommandResponse { out: string; outChunk: string; detailedStatus: any; - status: 'success' | 'failure' | 'inprogress' | 'terminated'; + status: CommandStatus; } @Injectable({ @@ -28,7 +36,19 @@ export class CommandRunner { readonly activeCommand$ = new BehaviorSubject(false); activeCommandId: string; - constructor(private readonly apollo: Apollo) {} + constructor( + private readonly apollo: Apollo, + contextualActionBarService: ContextualActionBarService + ) { + contextualActionBarService.contextualActions$.subscribe( + contextualActions => { + if (!contextualActions) { + this.activeCommandId = ''; + this.activeCommand$.next(false); + } + } + ); + } runCommand( mutation: DocumentNode, @@ -72,7 +92,7 @@ export class CommandRunner { ? JSON.parse(cc.detailedStatus) : null }; - if (c.status !== 'inprogress') { + if (c.status !== 'in-progress') { if (!dryRun) { this.activeCommand$.next(false); } @@ -154,6 +174,20 @@ export class CommandRunner { .subscribe(() => {}); } + removeAllCommands() { + return this.apollo + .mutate({ + mutation: gql` + mutation { + removeAllCommands { + result + } + } + ` + }) + .subscribe(() => {}); + } + removeCommand(id: string) { return this.apollo .mutate({ diff --git a/libs/utils/src/lib/is-node-js-installed.guard.ts b/libs/utils/src/lib/is-node-js-installed.guard.ts index 72a3ca34a7..6bae03cc93 100644 --- a/libs/utils/src/lib/is-node-js-installed.guard.ts +++ b/libs/utils/src/lib/is-node-js-installed.guard.ts @@ -24,10 +24,7 @@ export class IsNodeJsInstalledGuard implements CanActivateChild { ` }) .pipe( - map( - (v: { data: { isNodejsInstalled: { result: boolean } } }) => - v.data.isNodejsInstalled.result - ), + map((v: any) => v.data.isNodejsInstalled.result as boolean), tap(result => { if (!result) { this.router.navigate(['/install-nodejs']); diff --git a/nx.json b/nx.json index 158e5b6dc2..19be959bc3 100644 --- a/nx.json +++ b/nx.json @@ -36,6 +36,9 @@ }, "schema": { "tags": [] + }, + "feature-action-bar": { + "tags": [] } } } diff --git a/package.json b/package.json index 4d2f6c34f0..4abc0ec14a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ }, "license": "MIT", "scripts": { + "nx": "nx", + "ng": "ng", "start": "nps", "format": "nps format.write", "postinstall": "node ./tools/scripts/postinstall.js" diff --git a/server/src/api/commands.ts b/server/src/api/commands.ts index 8890315c79..304961babf 100644 --- a/server/src/api/commands.ts +++ b/server/src/api/commands.ts @@ -36,79 +36,99 @@ export class RecentCommands { this.commandInfos = this.withoutFirstCompletedCommand(this.commandInfos); } - this.commandInfos.push({ - id, - type, - workspace, - command, - status: 'waiting', - out: '', - outChunk: '', - factory, - detailedStatusCalculator, - commandRunning: null - }); + this.commandInfos = [ + { + id, + type, + workspace, + command, + status: 'waiting', + out: '', + outChunk: '', + factory, + detailedStatusCalculator, + commandRunning: null + }, + ...this.commandInfos + ]; } restartCommand(id: string) { const c = this.findMatchingCommand(id, this.commandInfos); - if (c.status === 'inprogress') { - this.stopCommand(id); + if (c) { + if (c.status === 'in-progress') { + this.stopCommands(this.getCommandById(id)); + } + c.out = ''; + c.outChunk = ''; + c.status = 'in-progress'; + c.commandRunning = c.factory(); } - c.out = ''; - c.outChunk = ''; - c.status = 'inprogress'; - c.commandRunning = c.factory(); } addOut(id: string, out: string) { const c = this.findMatchingCommand(id, this.commandInfos); - c.out += out; - c.outChunk += out; - try { - c.detailedStatusCalculator.addOut(out); - } catch (e) { - // Because detailedStatusCalculator are implemented - // without the build event protocol for now, they may fail. - // Console must remain working after their failure. - console.error('detailedStatusCalculator.addOut failed', e.message); + if (c) { + c.out += out; + c.outChunk += out; + try { + c.detailedStatusCalculator.addOut(out); + } catch (e) { + // Because detailedStatusCalculator are implemented + // without the build event protocol for now, they may fail. + // Console must remain working after their failure. + console.error('detailedStatusCalculator.addOut failed', e.message); + } } } // TOOD: vsavkin should convert status into an enum setFinalStatus(id: string, status: string) { const c = this.findMatchingCommand(id, this.commandInfos); - if (c.status === 'inprogress' || c.status === 'waiting') { - c.status = status; - } - try { - c.detailedStatusCalculator.setStatus(c.status as any); - } catch (e) { - // Because detailedStatusCalculator are implemented - // without the build event protocol for now, they may fail. - // Console must remain working after their failure. - console.error('detailedStatusCalculator.setStatus failed', e); + if (c) { + if (c.status === 'in-progress' || c.status === 'waiting') { + c.status = status; + } + try { + c.detailedStatusCalculator.setStatus(c.status as any); + } catch (e) { + // Because detailedStatusCalculator are implemented + // without the build event protocol for now, they may fail. + // Console must remain working after their failure. + console.error('detailedStatusCalculator.setStatus failed', e); + } } } - stopCommand(id: string) { - this.commandInfos.filter(c => c.id === id).forEach(c => { - if (c.status === 'inprogress') { + getCommandById(id: string) { + return this.commandInfos.filter(c => c.id === id); + } + + stopCommands(commands: CommandInformation[]) { + commands.forEach(c => { + if (c.status === 'in-progress') { + c.status = 'terminated'; + c.detailedStatusCalculator.setStatus('terminated'); if (os.platform() === 'win32') { c.commandRunning.kill(); } else { c.commandRunning.kill('SIGKILL'); } - c.status = 'terminated'; - c.detailedStatusCalculator.setStatus('terminated'); c.commandRunning = null; } }); } + removeAllCommands() { + const commandInfos = this.commandInfos; + this.commandInfos = []; + this.stopCommands(commandInfos); + } + removeCommand(id: string) { - this.stopCommand(id); + const commandToRemove = this.getCommandById(id); this.commandInfos = this.withoutCommandWithId(id, this.commandInfos); + this.stopCommands(commandToRemove); } private hasCompletedCommands(commands: CommandInformation[]) { @@ -132,17 +152,13 @@ export class RecentCommands { } private findMatchingCommand(id: string, commands: CommandInformation[]) { - const matchingCommand = this.commandInfos.find(c => c.id === id); - if (!matchingCommand) { - throw new Error(`Cannot find matching command: ${id}`); - } - return matchingCommand; + return this.commandInfos.find(c => c.id === id); } private isCompleted(c: CommandInformation): boolean { return ( - c.status === 'success' || - c.status === 'failure' || + c.status === 'successful' || + c.status === 'failed' || c.status === 'terminated' ); } diff --git a/server/src/api/run-command.ts b/server/src/api/run-command.ts index bb04545d20..afea251103 100644 --- a/server/src/api/run-command.ts +++ b/server/src/api/run-command.ts @@ -46,7 +46,7 @@ function createExecutableCommand( recentCommands.addOut(id, data.toString()); }); commandRunning.on('exit', (code: any) => { - recentCommands.setFinalStatus(id, code === 0 ? 'success' : 'failure'); + recentCommands.setFinalStatus(id, code === 0 ? 'successful' : 'failed'); }); return commandRunning; }; diff --git a/server/src/graphql-types.ts b/server/src/graphql-types.ts index 3145c655a8..0db6df1103 100644 --- a/server/src/graphql-types.ts +++ b/server/src/graphql-types.ts @@ -196,6 +196,7 @@ export interface Mutation { runNpm?: CommandStarted | null; stopCommand?: StopResult | null; removeCommand?: RemoveResult | null; + removeAllCommands?: RemoveResult | null; restartCommand?: RemoveResult | null; openInEditor?: OpenInEditor | null; updateSettings: Settings; @@ -1115,6 +1116,11 @@ export namespace MutationResolvers { runNpm?: RunNpmResolver; stopCommand?: StopCommandResolver; removeCommand?: RemoveCommandResolver; + removeAllCommands?: RemoveAllCommandsResolver< + RemoveResult | null, + any, + Context + >; restartCommand?: RestartCommandResolver; openInEditor?: OpenInEditorResolver; updateSettings?: UpdateSettingsResolver; @@ -1201,6 +1207,11 @@ export namespace MutationResolvers { id: string; } + export type RemoveAllCommandsResolver< + R = RemoveResult | null, + Parent = any, + Context = any + > = Resolver; export type RestartCommandResolver< R = RemoveResult | null, Parent = any, diff --git a/server/src/schema/resolvers.ts b/server/src/schema/resolvers.ts index a1e550a8ec..f7fc22ea24 100644 --- a/server/src/schema/resolvers.ts +++ b/server/src/schema/resolvers.ts @@ -308,7 +308,7 @@ const Mutation: MutationResolvers.Resolvers = { }, async stopCommand(_root: any, args: any) { try { - recentCommands.stopCommand(args.id); + recentCommands.stopCommands(recentCommands.getCommandById(args.id)); return { result: true }; } catch (e) { console.log(e); @@ -328,6 +328,15 @@ const Mutation: MutationResolvers.Resolvers = { throw new Error(`Error when removing commands. Message: "${e.message}"`); } }, + async removeAllCommands(_root: any, args: any) { + try { + recentCommands.removeAllCommands(); + return { result: true }; + } catch (e) { + console.log(e); + throw new Error(`Error when removing commands. Message: "${e.message}"`); + } + }, async restartCommand(_root: any, args: any) { try { recentCommands.restartCommand(args.id); diff --git a/server/src/schema/type-defs.ts b/server/src/schema/type-defs.ts index 8ef5438aca..24c1d2ce17 100644 --- a/server/src/schema/type-defs.ts +++ b/server/src/schema/type-defs.ts @@ -136,6 +136,7 @@ export const typeDefs = gql` ): CommandStarted stopCommand(id: String!): StopResult removeCommand(id: String!): RemoveResult + removeAllCommands: RemoveResult restartCommand(id: String!): RemoveResult openInEditor(editor: String!, path: String!): OpenInEditor updateSettings(data: String!): Settings! diff --git a/server/test/api/commands.spec.ts b/server/test/api/commands.spec.ts index 9caf62af2c..adfc8c2931 100644 --- a/server/test/api/commands.spec.ts +++ b/server/test/api/commands.spec.ts @@ -47,7 +47,7 @@ describe('RecentCommands', () => { createStatusCalculator() ); r.restartCommand('command3'); - r.setFinalStatus('command2', 'success'); + r.setFinalStatus('command2', 'successful'); r.addCommand( 'type', 'command4', @@ -59,9 +59,9 @@ describe('RecentCommands', () => { r.restartCommand('command4'); expect(r.commandInfos.map(c => c.id)).toEqual([ - 'command1', + 'command4', 'command3', - 'command4' + 'command1' ]); }); @@ -132,9 +132,9 @@ describe('RecentCommands', () => { () => {}, createStatusCalculator() ); - r.setFinalStatus('command1', 'success'); + r.setFinalStatus('command1', 'successful'); - expect(r.commandInfos[0].status).toEqual('success'); + expect(r.commandInfos[0].status).toEqual('successful'); }); it('should add out', () => { @@ -158,8 +158,8 @@ describe('RecentCommands', () => { r.addOut('command2', 'one'); r.addOut('command2', 'two'); - expect(r.commandInfos[1].out).toEqual('onetwo'); - expect(r.commandInfos[1].outChunk).toEqual('onetwo'); + expect(r.commandInfos[0].out).toEqual('onetwo'); + expect(r.commandInfos[0].outChunk).toEqual('onetwo'); }); it('should invoke calculator when adding out and setting status', () => { diff --git a/tsconfig.json b/tsconfig.json index 7be0118f9b..3d5b539f4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,12 +12,20 @@ "strictNullChecks": true, "noUnusedLocals": false, "noUnusedParameters": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, "noImplicitAny": true, "noImplicitThis": true, "strictFunctionTypes": true, - "typeRoots": ["node_modules/@types"], + "typeRoots": [ + "node_modules/@types" + ], "downlevelIteration": true, - "lib": ["es2017", "dom", "esnext.asynciterable"], + "lib": [ + "es2017", + "dom", + "esnext.asynciterable" + ], "baseUrl": ".", "paths": { "@angular-console/feature-install-node-js": [ @@ -32,12 +40,28 @@ "@angular-console/feature-generate": [ "libs/feature-generate/src/index.ts" ], - "@angular-console/utils": ["libs/utils/src/index.ts"], - "@angular-console/feature-run": ["libs/feature-run/src/index.ts"], - "@angular-console/ui": ["libs/ui/src/index.ts"], - "schema/*": ["dist/schema/*"], - "@angular-console/schema": ["libs/schema/src/index.ts"] + "@angular-console/utils": [ + "libs/utils/src/index.ts" + ], + "@angular-console/feature-run": [ + "libs/feature-run/src/index.ts" + ], + "@angular-console/ui": [ + "libs/ui/src/index.ts" + ], + "schema/*": [ + "dist/schema/*" + ], + "@angular-console/schema": [ + "libs/schema/src/index.ts" + ], + "@angular-console/feature-action-bar": [ + "libs/feature-action-bar/src/index.ts" + ] } }, - "exclude": ["node_modules", "tmp"] + "exclude": [ + "node_modules", + "tmp" + ] }