From b4c37f79996e081c9477e44952d502dc6c8ea2f2 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Wed, 24 Oct 2018 19:44:10 -0500 Subject: [PATCH 1/4] added snack bars to tell the user when the install status of one of their skills has changed --- .../src/app/skills/install.service.ts | 99 +++++++++++++------ .../install-button.component.ts | 10 +- .../skill-card/skill-card.component.scss | 4 + .../skill-card/skill-card.component.ts | 67 ++++++++++++- 4 files changed, 142 insertions(+), 38 deletions(-) diff --git a/market/frontend/v1/market-ui/src/app/skills/install.service.ts b/market/frontend/v1/market-ui/src/app/skills/install.service.ts index e624011a..e445ee2e 100644 --- a/market/frontend/v1/market-ui/src/app/skills/install.service.ts +++ b/market/frontend/v1/market-ui/src/app/skills/install.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from "rxjs"; import { AvailableSkill, SkillDetail } from "./skills.service"; +// Status values that can be expected in the install status endpoint response. type InstallStatus = 'failed' | 'installed' | 'installing' | 'uninstalling'; export interface SkillInstallStatus { @@ -18,11 +19,9 @@ export interface Installations { installStatuses: SkillInstallStatus; } -// const inProgressStatuses = ['installing', 'uninstalling', 'failed']; -const inProgressStatuses = ['installing', 'uninstalling']; +const inProgressStatuses = ['installing', 'uninstalling', 'failed']; const installStatusUrl = '/api/skill/installations'; -const installUrl = '/api/skill/install'; -const uninstallUrl = '/api/skill/uninstall'; +const installerSettingsUrl = '/api/skill/install'; @Injectable({ providedIn: 'root' @@ -32,7 +31,7 @@ export class InstallService { public installStatuses = new Subject(); public newInstallStatuses: SkillInstallStatus; private prevInstallStatuses: SkillInstallStatus; - public statusNotifications = new Subject(); + public statusNotifications = new Subject(); constructor(private http: HttpClient) { } @@ -52,7 +51,7 @@ export class InstallService { applyInstallStatusChanges() { if (this.prevInstallStatuses) { Object.keys(this.newInstallStatuses).forEach( - () => this.compareStatuses + (skillName) => {this.compareStatuses(skillName);} ); } this.prevInstallStatuses = this.newInstallStatuses; @@ -77,39 +76,41 @@ export class InstallService { compareStatuses(skillName: string) { let prevSkillStatus = this.prevInstallStatuses[skillName]; let newSkillStatus = this.newInstallStatuses[skillName]; - let statusNotifications: SkillInstallStatus = {}; switch (prevSkillStatus) { case ('installing'): { - if (['installed', 'failed'].includes(newSkillStatus)) { - statusNotifications[skillName] = newSkillStatus; + if (newSkillStatus === 'installed') { + this.statusNotifications.next([skillName, newSkillStatus]); + this.removeFromInstallQueue(skillName); + } else if (newSkillStatus === 'failed') { + this.statusNotifications.next([skillName, 'install failed']); } else { this.newInstallStatuses[skillName] = prevSkillStatus; } break; } case ('uninstalling'): { - if (!newSkillStatus || newSkillStatus === 'failed') { - statusNotifications[skillName] = newSkillStatus; + if (!newSkillStatus) { + this.statusNotifications.next([skillName, 'uninstalled']); + this.removeFromUninstallQueue(skillName); + } else if (newSkillStatus === 'failed') { + this.statusNotifications.next([skillName, 'uninstall failed']); } else { this.newInstallStatuses[skillName] = prevSkillStatus; } break; } case ('failed'): { - if (!newSkillStatus || newSkillStatus != 'installed') { - statusNotifications[skillName] = newSkillStatus; + if (!newSkillStatus) { + this.statusNotifications.next([skillName, 'uninstalled']); + } else if (newSkillStatus != 'installed') { + this.statusNotifications.next([skillName, newSkillStatus]); } else { this.newInstallStatuses[skillName] = prevSkillStatus; } break; } } - - if (statusNotifications) { - this.statusNotifications.next(statusNotifications) - } - } /*** @@ -142,7 +143,7 @@ export class InstallService { * the result of a requested install/uninstall. */ checkInstallationsInProgress() { - let inProgress = Object.values(this.installStatuses).filter( + let inProgress = Object.values(this.newInstallStatuses).filter( (installStatus) => inProgressStatuses.includes(installStatus) ); if (inProgress.length > 0) { @@ -151,26 +152,66 @@ export class InstallService { } /** - * Call the API endpoint for installing a skill. + * Call the API to add a skill to the Installer skill's "to_install" setting. * - * @param skill: the skill being installed + * @param skillName: the skill being installed */ - installSkill(skill: AvailableSkill): Observable { + addToInstallQueue(skillName: string): Observable { return this.http.put( - installUrl, - {skill_name: skill.name} + installerSettingsUrl, + { + action: "add", + section: "to_install", + skill_name: skillName + } ) } /** - * Call the API endpoint for uninstalling a skill. + * Call the API to add a skill to the Installer skill's "to_remove" setting. * - * @param skill: the skill being removed + * @param skillName: the skill being removed */ - uninstallSkill(skill: AvailableSkill): Observable { + addToUninstallQueue(skillName: string): Observable { return this.http.put( - uninstallUrl, - {skill_name: skill.name} + installerSettingsUrl, + { + action: "add", + section: "to_remove", + skill_name: skillName + } + ) + } + + /** + * Call the API to remove a skill to the Installer skill's "to_install" setting. + * + * @param skillName: the skill being installed + */ + removeFromInstallQueue(skillName: string): Observable { + return this.http.put( + installerSettingsUrl, + { + action: "remove", + section: "to_install", + skill_name: skillName + } + ) + } + + /** + * Call the API to remove a skill to the Installer skill's "to_remove" setting. + * + * @param skillName: the skill being removed + */ + removeFromUninstallQueue(skillName: string): Observable { + return this.http.put( + installerSettingsUrl, + { + action: "remove", + section: "to_remove", + skill_name: skillName + } ) } } diff --git a/market/frontend/v1/market-ui/src/app/skills/shared/install-button/install-button.component.ts b/market/frontend/v1/market-ui/src/app/skills/shared/install-button/install-button.component.ts index 258b6061..ad98cd36 100644 --- a/market/frontend/v1/market-ui/src/app/skills/shared/install-button/install-button.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/shared/install-button/install-button.component.ts @@ -11,7 +11,7 @@ import { faLock } from "@fortawesome/free-solid-svg-icons/faLock"; import { MatSnackBar } from "@angular/material"; const fiveSeconds = 5000; -const twentySeconds = 20000; +const tenSeconds = 10000; @Component({ @@ -59,7 +59,7 @@ export class InstallButtonComponent implements OnInit { * Install a skill onto one or many devices */ install_skill() : void { - this.installService.installSkill(this.skill).subscribe( + this.installService.addToInstallQueue(this.skill.name).subscribe( (response) => { this.onInstallSuccess(response) }, @@ -86,7 +86,7 @@ export class InstallButtonComponent implements OnInit { 'to your devices. Please allow up to two minutes for ' + 'installation to complete before using the skill.', null, - {panelClass: 'mycroft-snackbar', duration: twentySeconds} + {panelClass: 'mycroft-snackbar', duration: tenSeconds} ); } @@ -112,7 +112,7 @@ export class InstallButtonComponent implements OnInit { * Remove a skill from one or many devices */ uninstallSkill() : void { - this.installService.uninstallSkill(this.skill).subscribe( + this.installService.addToUninstallQueue(this.skill.name).subscribe( (response) => { this.onUninstallSuccess(response) }, @@ -136,7 +136,7 @@ export class InstallButtonComponent implements OnInit { 'uninstalling. Please allow up to a minute for the skill to be ' + 'removed from devices.', null, - {panelClass: 'mycroft-snackbar', duration: twentySeconds} + {panelClass: 'mycroft-snackbar', duration: tenSeconds} ); } } diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.scss b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.scss index cb84ce8c..d04da80a 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.scss +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.scss @@ -38,3 +38,7 @@ mat-card { mat-card:hover{ box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.2); } + +.login-snackbar { + text-align: center; +} \ No newline at end of file diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.ts b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.ts index 2ad14e5a..885cbaff 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-card/skill-card.component.ts @@ -2,18 +2,77 @@ * Format the header portion of a skill summary card. This includes the icon * for the skill and a Mycroft logo if the skill is authored by Mycroft AI. */ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit} from '@angular/core'; +import { MatSnackBar } from "@angular/material"; + import { AvailableSkill } from "../../skills.service"; +import { InstallService } from "../../install.service"; import { faComment } from "@fortawesome/free-solid-svg-icons"; +const fiveSeconds = 5000; + @Component({ selector: 'market-skill-card', templateUrl: './skill-card.component.html', styleUrls: ['./skill-card.component.scss'] }) -export class SkillCardComponent { +export class SkillCardComponent implements OnInit { @Input() public skill: AvailableSkill; public voiceIcon = faComment; - constructor() { } -} + constructor( + public installSnackbar: MatSnackBar, + private installService: InstallService) { + + } + + ngOnInit() { + this.installService.statusNotifications.subscribe( + (statusChange) => { + this.showStatusNotifications(statusChange); + } + ); + } + + showStatusNotifications(statusChange: string[]) { + let notificationMessage: string; + let [skillName, notificationStatus] = statusChange; + if (this.skill.name === skillName) { + switch (notificationStatus) { + case ('installed'): { + notificationMessage = 'The ' + this.skill.title + ' skill has ' + + 'been added to all your devices.'; + this.showInstallStatusNotification(notificationMessage); + break; + } + case ('uninstalled'): { + notificationMessage = 'The ' + this.skill.title + ' skill has ' + + 'been removed from all your devices.'; + this.showInstallStatusNotification(notificationMessage); + break; + } + case ('install failed'): { + notificationMessage = 'The ' + this.skill.title + ' failed to ' + + 'install to one or more of your devices. Install will be ' + + 'retried until successful'; + this.showInstallStatusNotification(notificationMessage); + break; + } + case ('uninstall failed'): { + notificationMessage = 'The ' + this.skill.title + ' failed to ' + + 'uninstall from one or more of your devices. Uninstall ' + + 'will be retried until successful'; + this.showInstallStatusNotification(notificationMessage) + } + } + } + } + + showInstallStatusNotification(notificationMessage: string) { + this.installSnackbar.open( + notificationMessage, + '', + {panelClass: 'login-snackbar', duration: fiveSeconds} + ) + } +} \ No newline at end of file From a167910feb6a63eeb03c9d8c0c3073c90fa83d16 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Thu, 25 Oct 2018 12:09:55 -0500 Subject: [PATCH 2/4] fixed bug where categories not included in search results were still displaying. --- .../skills/skill-summary/skill-search/skill-search.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-search/skill-search.component.ts b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-search/skill-search.component.ts index f99f2d4e..b6c76a9a 100644 --- a/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-search/skill-search.component.ts +++ b/market/frontend/v1/market-ui/src/app/skills/skill-summary/skill-search/skill-search.component.ts @@ -40,6 +40,8 @@ export class SkillSearchComponent implements OnInit, OnDestroy { searchSkills(): void { this.skillsService.searchSkills(this.searchTerm).subscribe( (skills) => { + this.skillsService.availableSkills = skills; + this.skillsService.getSkillCategories(); this.searchResults.emit(skills); } ); From 6c1da4ca671f80028741220a54707e4f50653c3c Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Fri, 26 Oct 2018 10:10:35 -0500 Subject: [PATCH 3/4] uncomment code that was commented for testing --- login/backend/v1/login-api/login_api/endpoints/logout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/login/backend/v1/login-api/login_api/endpoints/logout.py b/login/backend/v1/login-api/login_api/endpoints/logout.py index 52407c3a..a2c2bd63 100644 --- a/login/backend/v1/login-api/login_api/endpoints/logout.py +++ b/login/backend/v1/login-api/login_api/endpoints/logout.py @@ -14,7 +14,7 @@ class LogoutEndpoint(SeleneEndpoint): def __init__(self): super(LogoutEndpoint, self).__init__() - def put(self): + def get(self): try: self._authenticate() self._logout() From 990dc2166862c1cc438d51e438bf5c0a4c4dc5d7 Mon Sep 17 00:00:00 2001 From: Chris Veilleux Date: Fri, 26 Oct 2018 12:42:40 -0500 Subject: [PATCH 4/4] fix to previous fix to handle a skill with no triggers --- .../v1/market-api/market_api/endpoints/available_skills.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/market/backend/v1/market-api/market_api/endpoints/available_skills.py b/market/backend/v1/market-api/market_api/endpoints/available_skills.py index b7784ea3..4f414fa6 100644 --- a/market/backend/v1/market-api/market_api/endpoints/available_skills.py +++ b/market/backend/v1/market-api/market_api/endpoints/available_skills.py @@ -81,6 +81,9 @@ class AvailableSkillsEndpoint(SeleneEndpoint): def _reformat_skills(self, skills_to_include: List[RepositorySkill]): """Build the response data from the skill service response""" for skill in skills_to_include: + trigger = None + if skill.triggers: + trigger = skill.triggers[0] skill_info = dict( icon=skill.icon, iconImage=skill.icon_image, @@ -90,7 +93,7 @@ class AvailableSkillsEndpoint(SeleneEndpoint): name=skill.skill_name, summary=skill.summary, title=skill.title, - trigger=skill.triggers[0] + trigger=trigger ) self.response_skills.append(skill_info)