Merge pull request #2 from MycroftAI/dev

Tartarus overhaul to test
pull/3/head
Chris Veilleux 2019-03-02 14:45:06 -06:00 committed by GitHub
commit 2749058783
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
356 changed files with 21912 additions and 0 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
# Multistage Dockerfile to build the marketplace UI and a web server to run it
# STAGE ONE: build the marketplace angular application
FROM node:latest as build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
ARG selene_env
ARG application_name
RUN npm run build -- --project=globalnav
RUN npm run build-${selene_env} -- --project=${application_name}
# STAGE TWO: build the web server and copy the compiled angular app to it.
FROM nginx:latest
COPY --from=build /usr/src/app/dist/${application_name} /usr/share/nginx/html

99
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,99 @@
pipeline {
agent any
stages {
// Run the build in the against the dev branch to check for compile errors
stage('Build dev branch') {
when {
branch 'dev'
}
steps {
echo 'Building code in the "dev" branch...'
sh 'npm install'
sh 'ng build --project shared'
sh 'ng build --project globalnav'
sh 'ng build --project page-not-found'
sh 'ng build --project account --configuration development'
sh 'ng build --project sso --configuration development'
}
}
// Deploy to the Test environment
stage('Build for Test Environment') {
when {
branch 'test'
}
steps {
echo 'Building code in the "test" branch...'
sh 'npm install'
sh 'ng build --project shared'
sh 'ng build --project globalnav'
sh 'ng build --project page-not-found'
sh 'ng build --project account --configuration test'
sh 'ng build --project sso --configuration test'
}
}
stage('Deploy to Test Environment') {
when {
branch 'test'
}
steps {
echo 'Deploying to test environment web servers...'
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
// Deploy account application and its associated libraries
sh 'scp -r dist/shared root@192.81.211.55:/var/www/'
sh 'scp -r dist/globalnav root@192.81.211.55:/var/www/'
sh 'scp -r dist/page-not-found root@192.81.211.55:/var/www/'
sh 'scp -r dist/account root@192.81.211.55:/var/www/'
// Deploy single sign on application and its associated libraries
sh 'scp -r dist/shared root@198.199.90.118:/var/www/'
sh 'scp -r dist/globalnav root@198.199.90.118:/var/www/'
sh 'scp -r dist/page-not-found root@198.199.90.118:/var/www/'
sh 'scp -r dist/sso root@198.199.90.118:/var/www/'
}
}
}
// Deploy to the Production environment
stage('Build for Production Environment') {
when {
branch 'master'
}
steps {
echo 'Building code in the "master" branch...'
sh 'npm install'
sh 'ng build --project shared --prod'
sh 'ng build --project globalnav --prod'
sh 'ng build --project page-not-found'
sh 'ng build --project account --prod'
sh 'ng build --project sso --prod'
}
}
stage('Deploy to Production Environment') {
when {
branch 'master'
}
steps {
echo 'Deploying to production environment web servers...'
withCredentials([sshUserPrivateKey(credentialsId: '6413826d-79f6-4d03-9902-ee1b73a96efd', keyFileVariable: 'JENKINS_SSH_KEY', passphraseVariable: '', usernameVariable: 'SERVER_USER')]) {
// Deploy account application and its associated libraries
sh 'scp -r dist/shared root@???:/var/www/'
sh 'scp -r dist/globalnav root@???:/var/www/'
sh 'scp -r dist/page-not-found root@???:/var/www/'
sh 'scp -r dist/account root@???:/var/www/'
// Deploy single sign on application and its associated libraries
sh 'scp -r dist/shared root@???:/var/www/'
sh 'scp -r dist/globalnav root@???:/var/www/'
sh 'scp -r dist/page-not-found root@???:/var/www/'
sh 'scp -r dist/sso root@???:/var/www/'
}
}
}
}
}

794
angular.json Normal file
View File

@ -0,0 +1,794 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"internet": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/internet",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "internet:build"
},
"configurations": {
"production": {
"browserTarget": "internet:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "internet:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"internet-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "internet:serve"
},
"configurations": {
"production": {
"devServerTarget": "internet:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"market": {
"root": "projects/market/",
"sourceRoot": "projects/market/src",
"projectType": "application",
"prefix": "market",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:class": {
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:guard": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/market",
"index": "projects/market/src/index.html",
"main": "projects/market/src/main.ts",
"polyfills": "projects/market/src/polyfills.ts",
"tsConfig": "projects/market/tsconfig.app.json",
"assets": [
"projects/market/src/favicon.ico",
"projects/market/src/assets"
],
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "projects/market/src/environments/environment.ts",
"with": "projects/market/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"test": {
"fileReplacements": [
{
"replace": "projects/market/src/environments/environment.ts",
"with": "projects/market/src/environments/environment.test.ts"
}
],
"outputHashing": "all"
},
"development": {
"fileReplacements": [
{
"replace": "projects/market/src/environments/environment.ts",
"with": "projects/market/src/environments/environment.dev.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "market:build"
},
"configurations": {
"production": {
"browserTarget": "market:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "market:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/market/src/test.ts",
"polyfills": "projects/market/src/polyfills.ts",
"tsConfig": "projects/market/tsconfig.spec.json",
"karmaConfig": "projects/market/karma.conf.js",
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": [],
"assets": [
"projects/market/src/favicon.ico",
"projects/market/src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/market/tsconfig.app.json",
"projects/market/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"market-e2e": {
"root": "projects/market-e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "projects/market-e2e/protractor.conf.js",
"devServerTarget": "market:serve"
},
"configurations": {
"production": {
"devServerTarget": "market:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "projects/market-e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"sso": {
"root": "projects/sso/",
"sourceRoot": "projects/sso/src",
"projectType": "application",
"prefix": "sso",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:class": {
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:guard": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/sso",
"index": "projects/sso/src/index.html",
"main": "projects/sso/src/main.ts",
"polyfills": "projects/sso/src/polyfills.ts",
"tsConfig": "projects/sso/tsconfig.app.json",
"assets": [
"projects/sso/src/favicon.ico",
"projects/sso/src/assets"
],
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "projects/sso/src/environments/environment.ts",
"with": "projects/sso/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"test": {
"fileReplacements": [
{
"replace": "projects/sso/src/environments/environment.ts",
"with": "projects/sso/src/environments/environment.test.ts"
}
],
"outputHashing": "all"
},
"development": {
"fileReplacements": [
{
"replace": "projects/sso/src/environments/environment.ts",
"with": "projects/sso/src/environments/environment.dev.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "sso:build"
},
"configurations": {
"production": {
"browserTarget": "sso:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "sso:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/sso/src/test.ts",
"polyfills": "projects/sso/src/polyfills.ts",
"tsConfig": "projects/sso/tsconfig.spec.json",
"karmaConfig": "projects/sso/karma.conf.js",
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": [],
"assets": [
"projects/sso/src/favicon.ico",
"projects/sso/src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/sso/tsconfig.app.json",
"projects/sso/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"sso-e2e": {
"root": "projects/sso-e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "projects/sso-e2e/protractor.conf.js",
"devServerTarget": "sso:serve"
},
"configurations": {
"production": {
"devServerTarget": "sso:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "projects/sso-e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"globalnav": {
"root": "projects/globalnav",
"sourceRoot": "projects/globalnav/src",
"projectType": "library",
"prefix": "globalnav",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/globalnav/tsconfig.lib.json",
"project": "projects/globalnav/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/globalnav/src/test.ts",
"tsConfig": "projects/globalnav/tsconfig.spec.json",
"karmaConfig": "projects/globalnav/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/globalnav/tsconfig.lib.json",
"projects/globalnav/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"account": {
"root": "projects/account/",
"sourceRoot": "projects/account/src",
"projectType": "application",
"prefix": "account",
"schematics": {
"@schematics/angular:component": {
"styleext": "scss",
"spec": false
},
"@schematics/angular:class": {
"spec": false
},
"@schematics/angular:directive": {
"spec": false
},
"@schematics/angular:guard": {
"spec": false
},
"@schematics/angular:module": {
"spec": false
},
"@schematics/angular:pipe": {
"spec": false
},
"@schematics/angular:service": {
"spec": false
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/account",
"index": "projects/account/src/index.html",
"main": "projects/account/src/main.ts",
"polyfills": "projects/account/src/polyfills.ts",
"tsConfig": "projects/account/tsconfig.app.json",
"assets": [
"projects/account/src/favicon.ico",
"projects/account/src/assets"
],
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "projects/account/src/environments/environment.ts",
"with": "projects/account/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"development": {
"fileReplacements": [
{
"replace": "projects/account/src/environments/environment.ts",
"with": "projects/account/src/environments/environment.dev.ts"
}
],
"outputHashing": "all"
},
"test": {
"fileReplacements": [
{
"replace": "projects/account/src/environments/environment.ts",
"with": "projects/account/src/environments/environment.test.ts"
}
],
"outputHashing": "all"
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "account:build"
},
"configurations": {
"production": {
"browserTarget": "account:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "account:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/account/src/test.ts",
"polyfills": "projects/account/src/polyfills.ts",
"tsConfig": "projects/account/tsconfig.spec.json",
"karmaConfig": "projects/account/karma.conf.js",
"styles": [
"src/styles.scss",
"src/theme.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/stylesheets"
]
},
"scripts": [],
"assets": [
"projects/account/src/favicon.ico",
"projects/account/src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/account/tsconfig.app.json",
"projects/account/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"account-e2e": {
"root": "projects/account-e2e/",
"projectType": "application",
"prefix": "",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "projects/account-e2e/protractor.conf.js",
"devServerTarget": "account:serve"
},
"configurations": {
"production": {
"devServerTarget": "account:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "projects/account-e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"page-not-found": {
"root": "projects/page-not-found",
"sourceRoot": "projects/page-not-found/src",
"projectType": "library",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/page-not-found/tsconfig.lib.json",
"project": "projects/page-not-found/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/page-not-found/src/test.ts",
"tsConfig": "projects/page-not-found/tsconfig.spec.json",
"karmaConfig": "projects/page-not-found/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/page-not-found/tsconfig.lib.json",
"projects/page-not-found/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"shared": {
"root": "projects/shared",
"sourceRoot": "projects/shared/src",
"projectType": "library",
"prefix": "shared",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/shared/tsconfig.lib.json",
"project": "projects/shared/ng-package.json"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/shared/src/test.ts",
"tsConfig": "projects/shared/tsconfig.spec.json",
"karmaConfig": "projects/shared/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/shared/tsconfig.lib.json",
"projects/shared/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "internet"
}

28
e2e/protractor.conf.js Normal file
View File

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

14
e2e/src/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,14 @@
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to internet!');
});
});

11
e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

13
e2e/tsconfig.e2e.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

11733
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

66
package.json Normal file
View File

@ -0,0 +1,66 @@
{
"name": "internet",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build-dev": "ng build --configuration=development",
"build-test": "ng build --configuration=test",
"build-prod": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~7.0.0",
"@angular/cdk": "^7.0.1",
"@angular/common": "~7.0.0",
"@angular/compiler": "~7.0.0",
"@angular/core": "~7.0.1",
"@angular/flex-layout": "^7.0.0-beta.19",
"@angular/forms": "~7.0.0",
"@angular/http": "~7.0.0",
"@angular/material": "^7.0.1",
"@angular/platform-browser": "~7.0.0",
"@angular/platform-browser-dynamic": "~7.0.0",
"@angular/router": "~7.0.0",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.7",
"@fortawesome/free-brands-svg-icons": "^5.4.2",
"@fortawesome/free-solid-svg-icons": "^5.4.2",
"angular-6-social-login": "^1.1.1",
"angular-font-awesome": "^3.1.2",
"core-js": "^2.5.4",
"font-awesome": "^4.7.0",
"ngx-cookie-service": "^2.1.0",
"rxjs": "~6.3.3",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular-devkit/build-ng-packagr": "~0.10.0",
"@angular/cli": "~7.0.3",
"@angular/compiler-cli": "~7.0.0",
"@angular/language-service": "~7.0.0",
"@types/node": "~8.9.4",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"ng-packagr": "^4.6.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tsickle": ">=0.29.0",
"tslib": "^1.9.0",
"tslint": "~5.11.0",
"typescript": "~3.1.1"
}
}

View File

@ -0,0 +1,11 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

View File

@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View File

@ -0,0 +1,8 @@
{
"/api/*": {
"target": "http://localhost:5003",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CreateAccountComponent } from './create-account/create-account.component';
import { DeviceComponent } from './device/device.component';
import { PageNotFoundComponent } from 'page-not-found';
import { ProfileComponent } from './profile/profile.component';
import { SkillComponent } from './skill/skill.component';
const routes: Routes = [
{ path: 'create-account', component: CreateAccountComponent },
{ path: 'device', component: DeviceComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'skill', component: SkillComponent },
{ path: '', redirectTo: '/profile', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {
}

View File

@ -0,0 +1,5 @@
<globalnav-sidenav [mycroftUrls]="environment.mycroftUrls">
<div appBody id="account-body">
<router-outlet></router-outlet>
</div>
</globalnav-sidenav>

View File

@ -0,0 +1,19 @@
import { Component, OnInit } from '@angular/core';
import { environment } from '../environments/environment';
@Component({
selector: 'account-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
public environment = environment;
title = 'Account';
constructor() {
}
ngOnInit() {
}
}

View File

@ -0,0 +1,35 @@
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CreateAccountModule } from './create-account/create-account.module';
import { GlobalnavModule } from 'globalnav';
import { PageNotFoundModule } from 'page-not-found';
import { DeviceModule } from './device/device.module';
import { ProfileModule } from './profile/profile.module';
import { SharedModule } from 'shared';
import { SkillModule } from './skill/skill.module';
@NgModule(
{
declarations: [ AppComponent ],
imports: [
BrowserModule,
BrowserAnimationsModule,
CreateAccountModule,
GlobalnavModule,
HttpClientModule,
DeviceModule,
PageNotFoundModule,
ProfileModule,
SharedModule,
SkillModule,
AppRoutingModule
],
bootstrap: [ AppComponent ]
}
)
export class AppModule { }

View File

View File

@ -0,0 +1,14 @@
<h2 *ngIf="step === 'Terms of Use'" class="mat-h2">First, the legal stuff...</h2>
<h2 *ngIf="step === 'Privacy Policy'" class="mat-h2">We value your privacy!</h2>
<mat-card class="mat-elevation-z0" fxLayout="column" fxLayoutAlign="space-around">
<mat-card-content [innerHTML]="agreementContent"></mat-card-content>
<mat-card-actions align="right">
<button *ngIf="!agreementAccepted" mat-button (click)="acceptAgreement()">
Accept
</button>
<button *ngIf="agreementAccepted" class="accepted-button" mat-button (click)="declineAgreement()">
<fa-icon [icon]="acceptedIcon"></fa-icon>
Accepted
</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,36 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
@import "~src/stylesheets/components/buttons";
h2 {
margin-left: 16px;
margin-bottom: 0;
color: mat-color($mycroft-primary);
font-weight: bold;
}
mat-card {
height: 60vh;
margin-bottom: 16px;
margin-left: auto;
margin-right: auto;
max-width: 1000px;
mat-card-content {
max-height: 85%;
overflow-y: auto;
}
mat-card-actions {
padding-right: 8px;
}
}
button {
@include action-button-primary;
width: 120px;
}
.accepted-button {
background-color: mat-color($mycroft-accent, A100);
}

View File

@ -0,0 +1,48 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { Agreement, CreateAccountService } from '../create-account.service';
@Component({
selector: 'account-agreement-step',
templateUrl: './agreement-step.component.html',
styleUrls: ['./agreement-step.component.scss']
})
export class AgreementStepComponent implements OnInit {
public acceptedIcon = faCheck;
public agreementAccepted = false;
public agreementContent: SafeHtml;
@Input() newAcctForm: FormGroup;
@Input() step: string;
constructor(private newAcctService: CreateAccountService, private sanitizer: DomSanitizer) {
}
ngOnInit() {
this.newAcctService.getAgreement(this.step).subscribe(
(response) => { this.agreementContent = this.sanitizer.bypassSecurityTrustHtml(response.content); }
);
}
acceptAgreement() {
if (this.step === 'Terms of Use') {
this.newAcctForm.controls.termsOfUse.setValue(true);
} else {
this.newAcctForm.controls.privacyPolicy.setValue(true);
}
this.agreementAccepted = true;
}
declineAgreement () {
if (this.step === 'Terms of Use') {
this.newAcctForm.controls.termsOfUse.setValue(false);
} else {
this.newAcctForm.controls.privacyPolicy.setValue(false);
}
this.agreementAccepted = false;
}
}

View File

@ -0,0 +1,33 @@
<mat-card fxLayout.gt-sm="row" fxLayoutAlign="center center" class="mat-elevation-z0" [formGroup]="newAcctForm.get('login')">
<!-- Federated Log In Controls -->
<mat-card class="mat-elevation-z0">
<h2 class="mat-h2">Log In Using...</h2>
<p>{{federatedLoginText}}</p>
<div id="federated-buttons" fxLayout="column" fxLayoutAlign="center center">
<shared-google-button></shared-google-button>
<shared-facebook-button (facebookEmail)="onFacebookLogin($event)"></shared-facebook-button>
<shared-github-button></shared-github-button>
</div>
</mat-card>
<h1 class="mat-h1">OR</h1>
<!-- Mycroft Log In Controls-->
<mat-card class="mat-elevation-z0">
<h2 class="mat-h2">Email and Password</h2>
<p>{{internalLoginText}}</p>
<div fxLayout="column">
<mat-form-field [appearance]="'outline'">
<mat-label>Email Address</mat-label>
<input matInput type="email" formControlName="userEnteredEmail" [readonly]="disableInternal">
<mat-error>
Must be a valid email address
</mat-error>
</mat-form-field>
<mat-form-field [appearance]="'outline'">
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password" [readonly]="disableInternal">
</mat-form-field>
</div>
</mat-card>
</mat-card>

View File

@ -0,0 +1,31 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
.mat-h1 {
padding: 16px;
}
.mat-h2 {
color: mat-color($mycroft-primary);
font-weight: bold;
}
mat-card {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
mat-card {
height: 300px;
max-width: 400px;
#federated-buttons {
height: 180px;
}
}
}
mat-form-field {
max-width: 400px;
min-width: 300px;
}

View File

@ -0,0 +1,28 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'account-authentication-step',
templateUrl: './authentication-step.component.html',
styleUrls: ['./authentication-step.component.scss']
})
export class AuthenticationStepComponent implements OnInit {
public disableInternal = false;
public federatedLoginText: string;
public internalLoginText: string;
@Input() newAcctForm: FormGroup;
constructor() { }
ngOnInit() {
this.federatedLoginText = 'To use this option, you must allow the ' +
'provider to share your email address with Mycroft';
this.internalLoginText = 'Login credentials stored on Mycroft ' +
'servers are encrypted for your privacy and protection.';
}
onFacebookLogin(email: string) {
this.newAcctForm.patchValue({login: {federatedEmail: email}});
this.disableInternal = true;
}
}

View File

@ -0,0 +1,126 @@
<form [formGroup]="newAcctForm" (ngSubmit)="onFormSubmit()">
<!-- Show a horizontal stepper on larger devices -->
<mat-horizontal-stepper *ngIf="!alignVertical" labelPosition="bottom" [linear]="true">
<!-- Use font awesome icons in the stepper to indicate progress -->
<ng-template matStepperIcon="done">
<fa-icon [icon]="stepDoneIcon"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon [icon]="stepDoneIcon"></fa-icon>
</ng-template>
<mat-step label="Terms Of Use" [stepControl]="termsOfUseControl">
<account-agreement-step [newAcctForm]="newAcctForm" [step]="'Terms of Use'">
</account-agreement-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!termsOfUseControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Privacy Policy" [stepControl]="privacyPolicyControl">
<account-agreement-step [newAcctForm]="newAcctForm" [step]="'Privacy Policy'">
</account-agreement-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!privacyPolicyControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Login" [stepControl]="loginControl">
<account-authentication-step [newAcctForm]="newAcctForm"></account-authentication-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!loginControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Username" [stepControl]="usernameControl">
<account-username-step [newAcctForm]="newAcctForm"></account-username-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!usernameControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Support Mycroft" [stepControl]="supportControl">
<account-support-step [newAcctForm]="newAcctForm"></account-support-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!supportControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Done!">
<account-done-step [newAcctForm]="newAcctForm"></account-done-step>
</mat-step>
</mat-horizontal-stepper>
<!-- Show a vertical stepper on smaller devices -->
<mat-vertical-stepper *ngIf="alignVertical" [linear]="true">
<!-- Use font awesome icons in the stepper to indicate progress -->
<ng-template matStepperIcon="done">
<fa-icon [icon]="stepDoneIcon"></fa-icon>
</ng-template>
<ng-template matStepperIcon="edit">
<fa-icon [icon]="stepDoneIcon"></fa-icon>
</ng-template>
<mat-step label="Terms Of Use" [stepControl]="termsOfUseControl">
<account-agreement-step [newAcctForm]="newAcctForm" [step]="'Terms of Use'">
</account-agreement-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!termsOfUseControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Privacy Policy" [stepControl]="privacyPolicyControl">
<account-agreement-step [newAcctForm]="newAcctForm" [step]="'Privacy Policy'">
</account-agreement-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!privacyPolicyControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Log In Method" [stepControl]="loginControl">
<account-authentication-step [newAcctForm]="newAcctForm"></account-authentication-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!loginControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Display Name" [stepControl]="usernameControl">
<account-username-step [newAcctForm]="newAcctForm"></account-username-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!usernameControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Support Mycroft" [stepControl]="supportControl">
<account-support-step [newAcctForm]="newAcctForm"></account-support-step>
<div fxLayout="row" fxLayoutAlign="end">
<button mat-button matStepperNext type="button" [disabled]="!supportControl.valid">
Next
</button>
</div>
</mat-step>
<mat-step label="Done!">
<account-done-step [newAcctForm]="newAcctForm"></account-done-step>
</mat-step>
</mat-vertical-stepper>
</form>

View File

@ -0,0 +1,17 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
@import "~src/stylesheets/components/buttons";
button {
@include action-button-primary;
}
button:disabled {
background-color: mat-color($mycroft-accent, 200);
}
mat-horizontal-stepper {
margin-left: auto;
margin-right: auto;
max-width: 1200px;
}

View File

@ -0,0 +1,140 @@
import { Component, OnInit } from '@angular/core';
import { MediaChange, MediaObserver } from '@angular/flex-layout';
import {
AbstractControl,
FormBuilder,
FormGroup,
ValidatorFn,
Validators
} from '@angular/forms';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { MatSnackBar } from '@angular/material';
import { Subscription } from 'rxjs';
import {
CreateAccountService,
navigateToLogin,
storeRedirect
} from './create-account.service';
const noDelay = 0;
export function loginValidator(): ValidatorFn {
return (loginGroup: FormGroup) => {
let valid = true;
const federatedEmail = loginGroup.controls['federatedEmail'];
const userEnteredEmail = loginGroup.controls['userEnteredEmail'];
const password = loginGroup.controls['password'];
if (federatedEmail.value) {
if (userEnteredEmail.value || password.value) {
valid = false;
}
} else {
if (!userEnteredEmail.valid || !password.value) {
valid = false;
}
}
return valid ? null : {loginInvalid: true};
};
}
export function membershipValidator(): ValidatorFn {
return (supportGroup: FormGroup) => {
let valid = true;
const membershipType = supportGroup.controls['membership'];
const paymentAccountId = supportGroup.controls['paymentAccountId'];
if (membershipType.value !== 'MAYBE LATER') {
if (!paymentAccountId.value) {
valid = false;
}
}
return valid ? null : {membershipInvalid: true};
};
}
@Component({
selector: 'account-create-account',
templateUrl: './create-account.component.html',
styleUrls: ['./create-account.component.scss']
})
export class CreateAccountComponent implements OnInit {
public alignVertical: boolean;
public usernameControl: AbstractControl;
public loginControl: AbstractControl;
private mediaWatcher: Subscription;
public newAcctForm: FormGroup;
public privacyPolicyControl: AbstractControl;
public stepDoneIcon = faCheck;
public supportControl: AbstractControl;
public termsOfUseControl: AbstractControl;
constructor(
private formBuilder: FormBuilder,
public mediaObserver: MediaObserver,
private newAcctService: CreateAccountService,
private errorSnackbar: MatSnackBar
) {
this.mediaWatcher = mediaObserver.media$.subscribe(
(change: MediaChange) => {
this.alignVertical = ['xs', 'sm'].includes(change.mqAlias);
}
);
}
ngOnInit() {
storeRedirect();
this.buildForm();
this.setControlFormAliases();
}
private buildForm() {
const loginGroup = this.formBuilder.group(
{
federatedEmail: [null],
userEnteredEmail: [null, Validators.email],
password: [null]
},
{validator: loginValidator()}
);
const supportGroup = this.formBuilder.group(
{
openDataset: [null, Validators.required],
membership: [null, Validators.required],
paymentMethod: [null],
paymentAccountId: [null]
},
{validator: membershipValidator()}
);
this.newAcctForm = this.formBuilder.group({
username: ['', Validators.required],
privacyPolicy: [false, Validators.requiredTrue],
termsOfUse: [false, Validators.requiredTrue],
login: loginGroup,
support: supportGroup
});
this.newAcctForm.patchValue(
{support: {paymentMethod: 'Stripe', paymentAccountId: 'foostripe'}}
);
}
private setControlFormAliases() {
this.usernameControl = this.newAcctForm.controls['username'];
this.loginControl = this.newAcctForm.controls['login'];
this.privacyPolicyControl = this.newAcctForm.controls['privacyPolicy'];
this.supportControl = this.newAcctForm.controls['support'];
this.termsOfUseControl = this.newAcctForm.controls['termsOfUse'];
}
onFormSubmit() {
this.newAcctService.addAccount(this.newAcctForm).subscribe(
(response) => { navigateToLogin(noDelay); }
);
}
}

View File

@ -0,0 +1,54 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatStepperModule
} from '@angular/material';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { AuthenticationStepComponent } from './authentication-step/authentication-step.component';
import { AgreementStepComponent } from './agreement-step/agreement-step.component';
import { CreateAccountComponent } from './create-account.component';
import { CreateAccountService } from './create-account.service';
import { UsernameStepComponent } from './username-step/username-step.component';
import { SharedModule } from 'shared';
import { SupportStepComponent } from './support-step/support-step.component';
import { DoneStepComponent } from './done-step/done-step.component';
@NgModule({
declarations: [
CreateAccountComponent,
AgreementStepComponent,
UsernameStepComponent,
AuthenticationStepComponent,
SupportStepComponent,
DoneStepComponent
],
imports: [
CommonModule,
FontAwesomeModule,
FlexLayoutModule,
FormsModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatStepperModule,
ReactiveFormsModule,
SharedModule,
],
providers: [
CreateAccountService
]
})
export class CreateAccountModule { }

View File

@ -0,0 +1,83 @@
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { environment } from '../../environments/environment';
const accountUrl = '/api/account';
const agreementUrl = '/api/agreement/';
const fiveSeconds = 5000;
export interface Agreement {
type: string;
version: string;
content: string;
}
export function storeRedirect() {
localStorage.setItem(
'redirect',
decodeURIComponent(window.location.search).slice(10)
);
}
export function navigateToLogin(delay: number): void {
const redirectURI = localStorage.getItem('redirect');
const singleSignOnURI = environment.mycroftUrls.singleSignOn +
'/login?redirect=' +
redirectURI;
localStorage.removeItem('redirect');
setTimeout(() => { window.location.assign(singleSignOnURI); }, delay);
}
@Injectable({
providedIn: 'root'
})
export class CreateAccountService {
constructor(private http: HttpClient, private errorSnackbar: MatSnackBar) {
}
handleError(error: HttpErrorResponse) {
if (error.status === 400) {
this.errorSnackbar.open(
'Account creation failed.',
null,
{panelClass: 'mycroft-snackbar', duration: fiveSeconds}
);
}
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an observable with a user-facing error message
return throwError(
'Something bad happened; please try again later.');
}
getAgreement(agreementType: string) {
let url_suffix: string;
if (agreementType === 'Terms of Use') {
url_suffix = 'terms-of-use';
} else {
url_suffix = 'privacy-policy';
}
return this.http.get<Agreement>(agreementUrl + url_suffix);
}
addAccount(newAcctForm: FormGroup) {
return this.http.post<any>(accountUrl, newAcctForm.value).pipe(
catchError(this.handleError)
);
}
}

View File

@ -0,0 +1,5 @@
<mat-card fxLayout="column" fxLayoutAlign="center center" class="mat-elevation-z0">
<h1 class="mat-h1">You're Done!</h1>
<p class="mat-body">Your account is ready to be created. We are excited to have you as part of our community.</p>
<button mat-button>Create Account and Login</button>
</mat-card>

View File

@ -0,0 +1,17 @@
@import "~@angular/material/theming";
@import "~src/stylesheets/components/buttons";
mat-card {
margin-left: auto;
margin-right: auto;
max-width: 700px;
h1 {
color: mat-color($mycroft-primary);
}
button {
@include action-button-primary;
margin-top: 16px;
}
}

View File

@ -0,0 +1,17 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'account-done-step',
templateUrl: './done-step.component.html',
styleUrls: ['./done-step.component.scss']
})
export class DoneStepComponent implements OnInit {
@Input() newAcctForm: FormGroup;
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,18 @@
<mat-card class="mat-elevation-z0">
<h2 class="mat-h2">Join Mycroft's Open Dataset</h2>
<p *ngFor="let paragraph of openDatasetDescription" class="mat-body">
{{paragraph}}
</p>
<mat-button-toggle-group>
<mat-button-toggle (click)="onOptIn()">Opt Into the Mycroft Open Dataset</mat-button-toggle>
<mat-button-toggle (click)="onOptOut()">Maybe Later</mat-button-toggle>
</mat-button-toggle-group>
</mat-card>
<mat-card class="mat-elevation-z0">
<h2 class="mat-h2">Become a Member</h2>
<p *ngFor="let paragraph of membershipDescription" class="mat-body">
{{paragraph}}
</p>
<shared-membership-options (selectedMembership)="onMembershipSelection($event)"></shared-membership-options>
</mat-card>

View File

@ -0,0 +1,18 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
@import "~src/stylesheets/components/buttons";
.mat-h2 {
color: mat-color($mycroft-primary);
font-weight: bold;
}
mat-card {
margin-left: auto;
margin-right: auto;
max-width: 1000px;
}
mat-button-toggle-group {
@include options-button-group
}

View File

@ -0,0 +1,56 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'account-support-step',
templateUrl: './support-step.component.html',
styleUrls: ['./support-step.component.scss']
})
export class SupportStepComponent implements OnInit {
@Input() newAcctForm: FormGroup;
public openDatasetDescription: string[];
public membershipDescription: string[];
constructor() { }
ngOnInit() {
this.openDatasetDescription = [
'Mycroft\'s voices and services can only improve with your help. ' +
'By joining our open dataset, you agree to allow Mycroft AI to collect data related ' +
'to your interactions with devices running Mycroft\'s voice assistant software. ' +
'We pledge to use this contribution in a responsible way.',
'Your data will also be made available to other researchers in the ' +
'voice AI space with values that align with our own, like Mozilla Common Voice. ' +
'As part of their agreement with Mycroft AI to access this data, they will be ' +
'required to honor your request to remove any trace of your contributions if you ' +
'decide to opt out.',
'You can opt in or out of the open dataset at any time on your account profile page.',
'We thank you in advance for helping to improve Mycroft\'s services!'
];
this.membershipDescription = [
'Mycroft\'s voice assistant software is open source, which means it is free to use and ' +
'the underlying source code is available to the public. Our entire platform is free ' +
'of advertisements. The data we collect from those that opt in to our open dataset ' +
'is not sold to anyone.',
'While many contributions to the Mycroft platform come in the form ' +
'of volunteer work by community members, there is also a small team employed by Mycroft AI. ' +
'The team curates the software, supports the community and ensures the privacy of your data. ' +
'Your donation will help ensure our team can continue providing these services to our ' +
'users and community.',
'Members will receive benefits like access to premium voices.'
];
}
onOptIn() {
this.newAcctForm.patchValue({support: {openDataset: true}});
}
onOptOut() {
this.newAcctForm.patchValue({support: {openDataset: false}});
}
onMembershipSelection(selectedMembership: string) {
this.newAcctForm.patchValue({support: {membership: selectedMembership}});
}
}

View File

@ -0,0 +1,11 @@
<mat-card class="mat-elevation-z0">
<h2 class="mat-h2">What should we call you?</h2>
<p class="mat-body">{{whyUsernameParagraph}}</p>
<mat-form-field [appearance]="'outline'">
<mat-label>Display Name</mat-label>
<input matInput required type="text" [formControl]="usernameControl">
<mat-error *ngIf="usernameControl.hasError('required')">
Display Name is required
</mat-error>
</mat-form-field>
</mat-card>

View File

@ -0,0 +1,24 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
@import "~src/stylesheets/components/buttons";
.mat-h2 {
color: mat-color($mycroft-primary);
font-weight: bold;
}
mat-card {
margin-left: auto;
margin-right: auto;
max-width: 700px;
}
mat-form-field {
max-width: 400px;
min-width: 280px;
}
mat-button-toggle-group {
@include options-button-group
}

View File

@ -0,0 +1,24 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
@Component({
selector: 'account-username-step',
templateUrl: './username-step.component.html',
styleUrls: ['./username-step.component.scss']
})
export class UsernameStepComponent implements OnInit {
@Input() newAcctForm: FormGroup;
public whyUsernameParagraph: string;
public usernameControl: AbstractControl;
constructor() { }
ngOnInit() {
this.usernameControl = this.newAcctForm.controls.username;
this.whyUsernameParagraph = 'In some Mycroft web applications, like our community ' +
'forum, you will interact with other community members. In these cases, displaying ' +
'your email address to other users is not ideal. Your display name will be used instead ' +
'of your email address to identify you on these sites.';
}
}

View File

@ -0,0 +1,39 @@
<div mat-dialog-title class="mat-h2-primary">{{dialogTitle}}</div>
<div mat-dialog-content>
<div class="mat-body">{{dialogInstructions}}</div>
<mat-radio-group fxLayout="column" [(ngModel)]="dialogData">
<ng-container *ngFor="let possibleValue of possibleValues">
<!-- Radio buttons for pre-defined placements -->
<mat-radio-button *ngIf="possibleValue.preDefined" class="predefined-group" [value]="possibleValue.name">
{{possibleValue.name}}
</mat-radio-button>
<!-- Radio buttons for user-defined placements -->
<div *ngIf="!possibleValue.preDefined" fxLayout="row" fxLayoutAlign="space-between center">
<mat-radio-button [value]="possibleValue.name">
<mat-form-field [floatLabel]="'never'">
<input matInput class="user-defined-group" value="{{possibleValue.name}}">
</mat-form-field>
</mat-radio-button>
<button mat-icon-button [disableRipple]="true">
<fa-icon [icon]="deleteIcon"></fa-icon>
</button>
</div>
</ng-container>
<!-- Radio button to add a new user-defined group -->
<mat-radio-button>
<mat-form-field [floatLabel]="'never'">
<input matInput placeholder="Add {{dialogTitle}}">
</mat-form-field>
</mat-radio-button>
</mat-radio-group>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="onCancelClick()">CANCEL</button>
<button mat-button class="attribute-save-button" [mat-dialog-close]="dialogData">SAVE</button>
</div>

View File

@ -0,0 +1,22 @@
@import '~@angular/material/theming';
@import '~src/stylesheets/mycroft-colors';
@import '~src/stylesheets/components/buttons';
.predefined-group {
margin-bottom: 12px;
margin-top: 12px;
}
.mat-body{
margin-bottom: 16px;
width: 250px;
}
fa-icon {
color: mat-color($mycroft-warn);
margin-left: 16px;
}
.attribute-save-button {
@include action-button-primary;
}

View File

@ -0,0 +1,34 @@
import {Component, Input, OnInit} from '@angular/core';
import { MatDialogRef } from '@angular/material';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { DeviceAttribute } from '../device.service';
import { GeographyEditComponent } from './geography/geography-edit.component';
import { GroupEditComponent } from './group/group-edit.component';
import { PlacementEditComponent } from './placement/placement-edit.component';
@Component({
selector: 'account-device-attribute-edit',
templateUrl: './attr-edit.component.html',
styleUrls: ['./attr-edit.component.scss']
})
export class AttrEditComponent implements OnInit {
@Input() dialogData: string;
@Input() dialogInstructions: string;
@Input() dialogRef: MatDialogRef<GeographyEditComponent | GroupEditComponent | PlacementEditComponent>;
@Input() dialogTitle: string;
@Input() possibleValues: DeviceAttribute[];
public deleteIcon = faTrashAlt;
constructor() {
}
ngOnInit() {
}
onCancelClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,13 @@
<mat-form-field [appearance]="'outline'" (click)="onClick()">
<mat-label>{{label}}</mat-label>
<div fxLayout="row" fxLayoutAlign="none center">
<input
matInput
[readonly]="true"
type="text"
value="{{value.name}}"
>
<fa-icon matSuffix [icon]="editIcon" style="font-size: 16px;"></fa-icon>
</div>
<mat-hint>{{hint}}</mat-hint>
</mat-form-field>

View File

@ -0,0 +1,9 @@
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
fa-icon {
padding-right: 10px
}

View File

@ -0,0 +1,43 @@
import { Component, Input, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
@Component({
selector: 'account-device-attribute-view',
templateUrl: './attr-view.component.html',
styleUrls: ['./attr-view.component.scss']
})
export class AttrViewComponent implements OnInit {
@Input() editDialog: any;
@Input() hint: string;
@Input() label: string;
@Input() possibleValues: any[];
@Input() value: any;
public editIcon = faCaretRight;
constructor(private dialog: MatDialog) {
}
ngOnInit() {
}
onClick() {
const dialogRef = this.dialog.open(this.editDialog, { data: this.value.name });
dialogRef.afterClosed().subscribe(
(result) => { this.updateDevice(result); }
);
}
updateDevice(newValue: string) {
if (newValue) {
this.possibleValues.forEach(
(value) => {
if (value.name === newValue) {
this.value = value;
}
}
);
}
}
}

View File

@ -0,0 +1,43 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Geography'"
[possibleValues]="deviceGeographies"
>
</account-device-attribute-edit>
<!--<div mat-dialog-title class="mat-h2-primary">Geography</div>-->
<!--<div mat-dialog-content>-->
<!--<div class="mat-body">-->
<!--Groups are useful to organize multiple devices. You can reuse device names if they are in different groups.-->
<!--</div>-->
<!--<mat-radio-group fxLayout="column" [(ngModel)]="data">-->
<!--<ng-container *ngFor="let geo of deviceGeographies">-->
<!--&lt;!&ndash; Radio buttons for user-defined groups &ndash;&gt;-->
<!--<div fxLayout="row" fxLayoutAlign="space-between center">-->
<!--<mat-radio-button [value]="geo.name">-->
<!--<mat-form-field [floatLabel]="'never'">-->
<!--<input matInput value="{{geo.name}}">-->
<!--</mat-form-field>-->
<!--</mat-radio-button>-->
<!--<button mat-icon-button [disableRipple]="true">-->
<!--<fa-icon class="danger-icon" [icon]="deleteIcon"></fa-icon>-->
<!--</button>-->
<!--</div>-->
<!--</ng-container>-->
<!--&lt;!&ndash; Radio button to add a new user-defined group &ndash;&gt;-->
<!--<mat-radio-button>-->
<!--<mat-form-field [floatLabel]="'never'">-->
<!--<input matInput placeholder="Add Geographic Location">-->
<!--</mat-form-field>-->
<!--</mat-radio-button>-->
<!--</mat-radio-group>-->
<!--</div>-->
<!--<div mat-dialog-actions align="end">-->
<!--<button mat-button (click)="onCancelClick()">CANCEL</button>-->
<!--<button mat-button [mat-dialog-close]="data" class="action-button">SAVE</button>-->
<!--</div>-->

View File

@ -0,0 +1,25 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { DeviceAttribute, DeviceService} from '../../device.service';
@Component({
selector: 'account-device-geography-edit',
templateUrl: './geography-edit.component.html',
styleUrls: ['./geography-edit.component.scss']
})
export class GeographyEditComponent implements OnInit {
public deviceGeographies: DeviceAttribute[];
public dialogInstructions = '';
constructor(
private deviceService: DeviceService,
public dialogRef: MatDialogRef<GeographyEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) {
}
ngOnInit() {
this.deviceGeographies = this.deviceService.deviceGeographies;
}
}

View File

@ -0,0 +1,9 @@
<account-device-attribute-view
[editDialog]="dialog"
[hint]="'Country, postal code, time zone'"
[label]="'Geography'"
[possibleValues]="deviceGeographies"
[value]="device.location"
>
</account-device-attribute-view>

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import { Device, DeviceAttribute, DeviceService } from '../../device.service';
import { GeographyEditComponent } from './geography-edit.component';
@Component({
selector: 'account-device-geography-view',
templateUrl: './geography-view.component.html',
styleUrls: ['./geography-view.component.scss']
})
export class GeographyViewComponent implements OnInit {
@Input() device: Device;
public deviceGeographies: DeviceAttribute[];
public dialog = GeographyEditComponent;
constructor( private service: DeviceService) {
}
ngOnInit() {
this.deviceGeographies = this.service.deviceGeographies;
}
}

View File

@ -0,0 +1,8 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Group'"
[possibleValues]="deviceGroups"
>
</account-device-attribute-edit>

View File

@ -0,0 +1,25 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { DeviceAttribute, DeviceService} from '../../device.service';
@Component({
selector: 'account-device-group-edit',
templateUrl: './group-edit.component.html',
styleUrls: ['./group-edit.component.scss']
})
export class GroupEditComponent implements OnInit {
public deviceGroups: DeviceAttribute[];
public dialogInstructions = 'Groups are useful to organize multiple ' +
'devices. You can reuse device names if they are in different groups.';
constructor(
private deviceService: DeviceService,
public dialogRef: MatDialogRef<GroupEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: DeviceAttribute) {
}
ngOnInit() {
this.deviceGroups = this.deviceService.deviceGroups;
}
}

View File

@ -0,0 +1,9 @@
<account-device-attribute-view
[editDialog]="dialog"
[hint]="'Mechanism to categorize devices'"
[label]="'Group'"
[possibleValues]="deviceGroups"
[value]="device.group"
>
</account-device-attribute-view>

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import { Device, DeviceAttribute, DeviceService } from '../../device.service';
import { GroupEditComponent } from './group-edit.component';
@Component({
selector: 'account-device-group-view',
templateUrl: './group-view.component.html',
styleUrls: ['./group-view.component.scss']
})
export class GroupViewComponent implements OnInit {
@Input() device: Device;
public deviceGroups: DeviceAttribute[];
public dialog = GroupEditComponent;
constructor( private service: DeviceService) {
}
ngOnInit() {
this.deviceGroups = this.service.deviceGroups;
}
}

View File

@ -0,0 +1,8 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Placement'"
[possibleValues]="devicePlacements"
>
</account-device-attribute-edit>

View File

@ -0,0 +1,26 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { DeviceAttribute, DeviceService} from '../../device.service';
@Component({
selector: 'account-device-placement-edit',
templateUrl: './placement-edit.component.html',
styleUrls: ['./placement-edit.component.scss']
})
export class PlacementEditComponent implements OnInit {
public devicePlacements: DeviceAttribute[];
public dialogInstructions = 'You can optionally indicate where a device is ' +
'placed within a location. Field is informational only.';
constructor(
private deviceService: DeviceService,
public dialogRef: MatDialogRef<PlacementEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) {
}
ngOnInit() {
this.devicePlacements = this.deviceService.devicePlacements;
}
}

View File

@ -0,0 +1,9 @@
<account-device-attribute-view
[editDialog]="dialog"
[hint]="'Where a device is placed within a location'"
[label]="'Placement'"
[possibleValues]="devicePlacements"
[value]="device.placement"
>
</account-device-attribute-view>

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
import { PlacementEditComponent } from './placement-edit.component';
@Component({
selector: 'account-device-placement-view',
templateUrl: './placement-view.component.html',
styleUrls: ['./placement-view.component.scss']
})
export class PlacementViewComponent implements OnInit {
@Input() device: Device;
public devicePlacements: DeviceAttribute[];
public dialog = PlacementEditComponent;
constructor( private service: DeviceService) {
}
ngOnInit() {
this.devicePlacements = this.service.devicePlacements;
}
}

View File

@ -0,0 +1,8 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Voice'"
[possibleValues]="deviceVoices"
>
</account-device-attribute-edit>

View File

@ -0,0 +1,27 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { DeviceAttribute, DeviceService} from '../../device.service';
@Component({
selector: 'account-device-voice-edit',
templateUrl: './voice-edit.component.html',
styleUrls: ['./voice-edit.component.scss']
})
export class VoiceEditComponent implements OnInit {
public deviceVoices: DeviceAttribute[];
public dialogInstructions = 'Mycroft\'s voice technology is rapidly ' +
'evolving. Local voices guarantee the most privacy. Premium voices ' +
'are more natural but require an internet connection.';
constructor(
private deviceService: DeviceService,
public dialogRef: MatDialogRef<VoiceEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) {
}
ngOnInit() {
this.deviceVoices = this.deviceService.deviceVoices;
}
}

View File

@ -0,0 +1,9 @@
<account-device-attribute-view
[editDialog]="dialog"
[hint]="'Select the voice used by your device'"
[label]="'Voice'"
[possibleValues]="deviceVoices"
[value]="device.voice"
>
</account-device-attribute-view>

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
import { VoiceEditComponent } from './voice-edit.component';
@Component({
selector: 'account-device-voice-view',
templateUrl: './voice-view.component.html',
styleUrls: ['./voice-view.component.scss']
})
export class VoiceViewComponent implements OnInit {
@Input() device: Device;
public deviceVoices: DeviceAttribute[];
public dialog = VoiceEditComponent;
constructor( private service: DeviceService) {
}
ngOnInit() {
this.deviceVoices = this.service.deviceVoices;
}
}

View File

@ -0,0 +1,8 @@
<account-device-attribute-edit
[dialogData]="data"
[dialogInstructions]="dialogInstructions"
[dialogRef]="dialogRef"
[dialogTitle]="'Wake Word'"
[possibleValues]="deviceWakeWords"
>
</account-device-attribute-edit>

View File

@ -0,0 +1,27 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { DeviceAttribute, DeviceService} from '../../device.service';
@Component({
selector: 'account-device-wake-word-edit',
templateUrl: './wake-word-edit.component.html',
styleUrls: ['./wake-word-edit.component.scss']
})
export class WakeWordEditComponent implements OnInit {
public deviceWakeWords: DeviceAttribute[];
public dialogInstructions = 'Mycroft\'s voice technology is rapidly ' +
'evolving. Local voices guarantee the most privacy. Premium voices ' +
'are more natural but require an internet connection.';
constructor(
private deviceService: DeviceService,
public dialogRef: MatDialogRef<WakeWordEditComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) {
}
ngOnInit() {
this.deviceWakeWords = this.deviceService.deviceWakeWords;
}
}

View File

@ -0,0 +1,9 @@
<account-device-attribute-view
[editDialog]="dialog"
[hint]="'Select the wake word used by your device'"
[label]="'Wake Word'"
[possibleValues]="deviceWakeWords"
[value]="device.wakeWord"
>
</account-device-attribute-view>

View File

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import {Device, DeviceAttribute, DeviceService} from '../../device.service';
import { WakeWordEditComponent} from './wake-word-edit.component';
@Component({
selector: 'account-device-wake-word-view',
templateUrl: './wake-word-view.component.html',
styleUrls: ['./wake-word-view.component.scss']
})
export class WakeWordViewComponent implements OnInit {
@Input() device: Device;
public deviceWakeWords: DeviceAttribute[];
public dialog = WakeWordEditComponent;
constructor( private service: DeviceService) {
}
ngOnInit() {
this.deviceWakeWords = this.service.deviceVoices;
}
}

View File

@ -0,0 +1,67 @@
<div id="add-device-button" fxLayout="row" fxLayoutAlign="start center">
<img src="../assets/generic-device-icon-blue.svg">
<span fxFlex class="mat-h2">ADD DEVICE</span>
<fa-icon class="mat-h2" [icon]="addIcon"></fa-icon>
</div>
<!-- Device listing - show summary in expansion panel header and editable fields in expansion panel detail -->
<mat-expansion-panel *ngFor="let device of devices">
<!-- Put the platform icon, device name and device placement in the panel header -->
<mat-expansion-panel-header [expandedHeight]="'100px'" [collapsedHeight]="'100px'">
<div fxLayout="row" fxLayoutAlign="start" fxLayoutGap="16px">
<img [src]="getDeviceIcon(device)"/>
<div>
<div class="mat-h2">{{device.name}}</div>
<div class="mat-subheader">{{device.placement.name}}</div>
</div>
</div>
</mat-expansion-panel-header>
<div id="device-settings" fxLayout.xs="row wrap">
<div fxLayout="row wrap" fxLayoutGap.gt-xs="16px">
<!-- Navigation to skill settings for this device. -->
<mat-form-field fxFlex.xl=40 fxFlex.lt-xl [appearance]="'outline'">
<mat-label>Name</mat-label>
<input
id="deviceName"
matInput
name="deviceName"
required
type="text"
value="{{device.name}}"
>
<mat-hint>Must be unique within a device group (if defined)</mat-hint>
</mat-form-field>
<account-device-group-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-group-view>
<account-device-geography-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-geography-view>
<account-device-placement-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-placement-view>
<account-device-voice-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-voice-view>
<account-device-wake-word-view fxFlex.xl=40 fxFlex.lt-xl [device]="device"></account-device-wake-word-view>
</div>
<div fxFlex.xl="25" fxFlex.lt-xl>
<div fxLayout="column">
<!-- Static fields that display platform, software version and hardware version -->
<div>
<div *ngFor="let staticData of defineStaticDeviceFields(device)">
<span class="mat-body">{{staticData.name}}:&nbsp;&nbsp;</span>
<span class="mat-body-primary">{{staticData.value}}</span>
</div>
</div>
<!-- Last but not least, the delete device button -->
<button mat-flat-button color="primary" class="settings-button">
<fa-icon [icon]="settingsIcon"></fa-icon>
SKILL SETTINGS
</button>
<button mat-flat-button color="warn" class="delete-button" (click)="onRemovalClick(device)">
<fa-icon [icon]="deleteIcon"></fa-icon>
REMOVE DEVICE
</button>
</div>
</div>
</div>
</mat-expansion-panel>

View File

@ -0,0 +1,77 @@
@import '~@angular/material/theming';
@import '~src/stylesheets/mycroft-colors';
@import '~src/stylesheets/components/buttons';
@mixin panel-defaults {
border-radius: 12px;
max-width: 1000px;
min-width: 250px;
margin-left: auto;
margin-right: auto;
}
#add-device-button {
@include action-button-primary;
@include panel-defaults;
border-radius: 12px;
cursor: pointer;
height: 50px;
margin-top: 32px;
img {
height: 32px;
margin-left: 16px;
}
.mat-h2 {
margin-bottom: 0;
}
fa-icon {
color: mat-color($mycroft-accent, 'A200');
margin-right: 16px;
}
}
mat-expansion-panel {
@include panel-defaults;
margin-top: 16px;
button {
width: 100%;
}
fa-icon {
padding-right: 10px
}
img {
height: 60px;
}
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
//.delete-button {
// margin-top: 20px;
//}
.mat-body-primary {
margin-bottom: 0;
}
.mat-h2 {
color: mat-color($mycroft-primary);
margin-bottom: 0;
}
.mat-subheader {
padding: 0;
}
.settings-button {
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { faCog, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { DeviceService, Device } from '../device.service';
import { RemoveComponent } from '../remove/remove.component';
@Component({
selector: 'account-device-list',
templateUrl: './device-list.component.html',
styleUrls: ['./device-list.component.scss']
})
export class DeviceListComponent implements OnInit {
public addIcon = faPlus;
public deleteIcon = faTrash;
public devices: Device[];
public platforms = {
'mark-one': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
'mark-two': {icon: '../assets/mark-2-icon.svg', displayName: 'Mark II'},
'picroft': {icon: '../assets/picroft-icon.svg', displayName: 'Picroft'},
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
};
private selectedDevice: Device;
public settingsIcon = faCog;
constructor(public dialog: MatDialog, private deviceService: DeviceService) { }
ngOnInit() {
this.devices = this.deviceService.devices;
}
onRemovalClick (device: Device) {
const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
this.selectedDevice = device;
removalDialogRef.afterClosed().subscribe(
(result) => {
if (result) { this.deviceService.deleteDevice(device); }
}
);
}
defineStaticDeviceFields(device: Device) {
const knownPlatform = this.platforms[device.platform];
return [
{name: 'Platform', value: knownPlatform ? knownPlatform.displayName : device.platform},
{name: 'Core Version', value: device.coreVersion},
{name: 'Enclosure Version', value: device.enclosureVersion}
];
}
getDeviceIcon(device: Device) {
const knownPlatform = this.platforms[device.platform];
// TODO: get unknown product icon from design team.
return knownPlatform ? knownPlatform.icon : '../assets/mark-1-icon.svg';
}
}

View File

@ -0,0 +1,10 @@
<div fxLayout="row" fxLayoutAlign="center center">
<mat-tab-group mat-stretch-tabs>
<mat-tab [label]="'Devices'">
<account-device-list></account-device-list>
</mat-tab>
<mat-tab [label]="'Preferences'">
<account-device-preferences></account-device-preferences>
</mat-tab>
</mat-tab-group>
</div>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { DeviceService, Device } from './device.service';
import { RemoveComponent } from './remove/remove.component';
@Component({
selector: 'account-device',
templateUrl: './device.component.html',
styleUrls: ['./device.component.scss']
})
export class DeviceComponent implements OnInit {
public addIcon = faPlus;
public deleteIcon = faTrash;
public devices: Device[];
public platforms = {
'mark-one': {icon: '../assets/mark-1-icon.svg', displayName: 'Mark I'},
'mark-two': {icon: '../assets/mark-2-icon.svg', displayName: 'Mark II'},
'picroft': {icon: '../assets/picroft-icon.svg', displayName: 'Picroft'},
'kde': {icon: '../assets/kde-icon.svg', displayName: 'KDE'}
};
private selectedDevice: Device;
constructor(public dialog: MatDialog, private deviceService: DeviceService) { }
ngOnInit() {
this.devices = this.deviceService.devices;
}
onRemovalClick (device: Device) {
const removalDialogRef = this.dialog.open(RemoveComponent, {data: false});
this.selectedDevice = device;
removalDialogRef.afterClosed().subscribe(
(result) => {
if (result) { this.deviceService.deleteDevice(device); }
}
);
}
defineStaticDeviceFields(device: Device) {
const knownPlatform = this.platforms[device.platform];
return [
{name: 'Platform', value: knownPlatform ? knownPlatform.displayName : device.platform},
{name: 'Core Version', value: device.coreVersion},
{name: 'Enclosure Version', value: device.enclosureVersion}
];
}
getDeviceIcon(device: Device) {
const knownPlatform = this.platforms[device.platform];
// TODO: get unknown product icon from design team.
return knownPlatform ? knownPlatform.icon : '../assets/mark-1-icon.svg';
}
}

View File

@ -0,0 +1,91 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule } from '@angular/forms';
import {
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDialogModule,
MatExpansionModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatSelectModule,
MatSlideToggleModule,
MatTabsModule,
MatToolbarModule
} from '@angular/material';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { AttrEditComponent } from './attribute/attr-edit.component';
import { AttrViewComponent } from './attribute/attr-view.component';
import { DeviceComponent } from './device.component';
import { DeviceListComponent } from './device-list/device-list.component';
import { DeviceService } from './device.service';
import { GeographyEditComponent } from './attribute/geography/geography-edit.component';
import { GeographyViewComponent } from './attribute/geography/geography-view.component';
import { GroupEditComponent } from './attribute/group/group-edit.component';
import { GroupViewComponent } from './attribute/group/group-view.component';
import { PlacementEditComponent } from './attribute/placement/placement-edit.component';
import { PlacementViewComponent } from './attribute/placement/placement-view.component';
import { RemoveComponent } from './remove/remove.component';
import { VoiceEditComponent } from './attribute/voice/voice-edit.component';
import { VoiceViewComponent } from './attribute/voice/voice-view.component';
import { PreferencesComponent } from './preferences/preferences.component';
import { WakeWordEditComponent } from './attribute/wake-word/wake-word-edit.component';
import { WakeWordViewComponent } from './attribute/wake-word/wake-word-view.component';
@NgModule({
declarations: [
AttrEditComponent,
AttrViewComponent,
DeviceComponent,
DeviceListComponent,
GeographyEditComponent,
GeographyViewComponent,
GroupEditComponent,
GroupViewComponent,
PlacementEditComponent,
PlacementViewComponent,
RemoveComponent,
VoiceEditComponent,
VoiceViewComponent,
PreferencesComponent,
WakeWordEditComponent,
WakeWordViewComponent
],
entryComponents: [
GeographyEditComponent,
GroupEditComponent,
PlacementEditComponent,
RemoveComponent,
VoiceEditComponent,
WakeWordEditComponent
],
imports: [
CommonModule,
DragDropModule,
FlexLayoutModule,
FontAwesomeModule,
FormsModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDialogModule,
MatExpansionModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatSelectModule,
MatSlideToggleModule,
MatTabsModule,
MatToolbarModule
],
providers: [
DeviceService
]
})
export class DeviceModule { }

View File

@ -0,0 +1,113 @@
import { Injectable } from '@angular/core';
export interface DeviceAttribute {
id?: string;
name: string;
preDefined: boolean;
}
export interface Device {
coreVersion: string;
enclosureVersion: string;
group: DeviceAttribute;
id: string;
location: DeviceAttribute;
name: string;
placement: DeviceAttribute;
platform: string;
voice: DeviceAttribute;
wakeWord: DeviceAttribute;
}
@Injectable({
providedIn: 'root'
})
export class DeviceService {
public devices: Device[] = [
{
coreVersion: '18.08',
enclosureVersion: '1.2.3',
group: {id: '1', name: 'None', preDefined: true},
id: 'abc-def-ghi',
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
name: 'Mark',
placement: {id: 'bbb-bbb-bbb', name: 'Living Room', preDefined: false},
platform: 'mark-one',
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
wakeWord: {id: '1a2b-3c4d-5e6f', name: 'Hey Mycroft', preDefined: true},
},
{
coreVersion: '18.08',
enclosureVersion: '1.2.3',
group: {id: '1', name: 'None', preDefined: true},
id: 'bcd-efg-hij',
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
name: 'Marky Mark',
placement: {id: 'bbb-bbb-bbb', name: 'Kitchen', preDefined: true},
platform: 'mark-two',
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
wakeWord: {id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true}
},
{
coreVersion: '18.08',
enclosureVersion: '1.2.3',
group: {id: '2', name: 'Parent House', preDefined: false},
id: 'cde-fgh-ijk',
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
name: 'American Pie',
placement: {id: 'ddd-ddd-ddd', name: 'Bedroom', preDefined: true},
platform: 'picroft',
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
wakeWord: {id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true}
},
{
coreVersion: '18.08',
enclosureVersion: '1.2.3',
group: {id: '2', name: 'Parent House', preDefined: false},
id: 'def-ghi-jkl',
location: {id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
name: 'Kappa Delta Epsilon',
placement: {id: 'fff-fff-fff', name: 'Kitchen', preDefined: true},
platform: 'kde',
voice: {id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
wakeWord: {id: 'abcd-efgh-ijkl', name: 'Hey Jarvis', preDefined: true}
}
];
public deviceGroups: DeviceAttribute[] = [
{ id: '1', name: 'None', preDefined: true},
{ id: null, name: 'Home', preDefined: true},
{ id: null, name: 'Office', preDefined: true},
{ id: '2', name: 'Parent House', preDefined: false}
];
public devicePlacements: DeviceAttribute[] = [
{ id: '1', name: 'None', preDefined: true},
{ id: null, name: 'Bedroom', preDefined: true},
{ id: null, name: 'Kitchen', preDefined: true},
{ id: '2', name: 'Living Room', preDefined: false}
];
public deviceGeographies: DeviceAttribute[] = [
{id: '1a2b-3c4d-5e6f', name: 'United States, 64101, CST', preDefined: false},
{id: 'a1b2-c3d4-e5f6', name: 'United Kingdom, ABCDE, BST', preDefined: false}
];
public deviceVoices: DeviceAttribute[] = [
{id: '1a2b-3c4d-5e6f', name: 'British Male', preDefined: true},
{id: 'a1b2-c3d4-e5f6', name: 'American Female', preDefined: true},
{id: 'abcd-efgh-ijkl', name: 'American Male', preDefined: true}
];
public deviceWakeWords: DeviceAttribute[] = [
{id: '1a2b-3c4d-5e6f', name: 'Hey Mycroft', preDefined: true},
{id: 'a1b2-c3d4-e5f6', name: 'Christopher', preDefined: true},
{id: 'abcd-efgh-ijkl', name: 'Hey Jarvis', preDefined: true}
];
constructor() { }
deleteDevice(device: Device): void {
console.log('deleting device... ');
}
}

View File

@ -0,0 +1,51 @@
<mat-card id="basic-settings-card">
<mat-toolbar>
<span class="section-card-title">Basic Settings</span>
</mat-toolbar>
<div fxLayout="row wrap" fxLayoutAlign="space-between center" class="section-content">
<mat-slide-toggle [labelPosition]="'before'">Use device groups</mat-slide-toggle>
<span>
<mat-label>Measurement System</mat-label>
<mat-button-toggle-group>
<mat-button-toggle>Imperial</mat-button-toggle>
<mat-button-toggle>Metric</mat-button-toggle>
</mat-button-toggle-group>
</span>
<span>
<mat-label>Time Format</mat-label>
<mat-button-toggle-group>
<mat-button-toggle>12 Hour</mat-button-toggle>
<mat-button-toggle>24 Hour</mat-button-toggle>
</mat-button-toggle-group>
</span>
<span>
<mat-label>Date Format</mat-label>
<mat-button-toggle-group>
<mat-button-toggle>DD/MM/YYYY</mat-button-toggle>
<mat-button-toggle>MM/DD/YYYY</mat-button-toggle>
</mat-button-toggle-group>
</span>
</div>
</mat-card>
<mat-card id="default-settings-card">
<mat-toolbar>
<span>Defaults</span>
</mat-toolbar>
<div fxLayout="column" fxLayoutAlign="start start" class="section-content">
<mat-label>Default Location</mat-label>
<mat-select placeholder="None Selected"></mat-select>
</div>
</mat-card>
<mat-card id="advanced-settings-card">
<mat-toolbar>
<span class="section-card-title">Advanced Settings</span>
</mat-toolbar>
<div fxLayout="column" class="section-content">
<div class="mat-body" *ngFor="let paragraph of advancedSettingsDesc">
<p>{{paragraph}}</p>
</div>
<button mat-flat-button>VIEW DOCUMENTATION</button>
</div>
</mat-card>

View File

@ -0,0 +1,33 @@
@import "~src/stylesheets/components/buttons";
@import "~src/stylesheets/components/cards";
#basic-settings-card {
@include section-card;
margin-top: 16px;
mat-button-toggle-group {
@include options-button-group;
mat-button-toggle {
width: 130px;
}
}
mat-label {
margin-bottom: 8px;
margin-top: 16px;
}
}
#default-settings-card {
@include section-card;
}
#advanced-settings-card {
@include section-card;
button {
@include action-button-primary;
margin-left: auto;
margin-right: auto;
margin-top: 16px;
}
}

View File

@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'account-device-preferences',
templateUrl: './preferences.component.html',
styleUrls: ['./preferences.component.scss']
})
export class PreferencesComponent implements OnInit {
public advancedSettingsDesc = [
'Mycroft Core can be further configured ' +
'for development and experimentation purposes. Example configurations ' +
'include text-to-speech technologies, speech-to-text technologies and ' +
'wake word listeners.',
'These advanced options can be managed by editing a configuration file ' +
'on the device. Proceed with caution; a bad configuration file could ' +
'render your device unusable.',
'Follow the link below for documentation on the options available ' +
'and how to edit them.'
];
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,12 @@
<div mat-dialog-title class="mat-h2-primary">Remove Device</div>
<div mat-dialog-content>
<div class="mat-body">
Just double checking. Device removal cannot be undone.
</div>
</div>
<div mat-dialog-actions align="end">
<button id="device-remove-cancel-button" mat-button (click)="onCancelClick()">CANCEL</button>
<button id="device-remove-button" mat-button [mat-dialog-close]="true">REMOVE</button>
</div>

View File

@ -0,0 +1,10 @@
@import '~src/stylesheets/components/buttons';
.mat-body{
margin-bottom: 16px;
width: 250px;
}
#device-remove-button {
@include action-button-warn;
}

View File

@ -0,0 +1,22 @@
import {Component, Inject, OnInit} from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'account-device-remove',
templateUrl: './remove.component.html',
styleUrls: ['./remove.component.scss']
})
export class RemoveComponent implements OnInit {
constructor(
public dialogRef: MatDialogRef<RemoveComponent>,
@Inject(MAT_DIALOG_DATA) public data: boolean) {
}
ngOnInit() {
}
onCancelClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,21 @@
<mat-card>
<mat-toolbar>
<span>Agreements</span>
</mat-toolbar>
<mat-card-content fxLayout="column" fxLayoutAlign="start none">
<ng-container *ngFor="let agreement of account.agreements">
<div *ngIf="agreement.type !== 'Open Dataset'" class="profile-text" fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="start center">
<span class="mat-subheading-2">{{agreement.type}}:</span>
<p class="mat-body">Accepted {{agreement.acceptDate}}</p>
</div>
</ng-container>
<!--<div fxLayout="row" fxLayoutAlign="start center">-->
<!--<span class="mat-subheading-2">Username:</span>-->
<!--<p class="mat-body">{{agreement.username}}</p>-->
<!--</div>-->
<!--<div fxLayout="column" fxLayoutAlign="center">-->
<!--<button mat-button>CHANGE USERNAME</button>-->
<!--<button mat-button>CHANGE PASSWORD</button>-->
<!--</div>-->
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,26 @@
@import "~src/stylesheets/components/cards";
mat-card {
@include section-card;
mat-card-content {
margin-left: 16px;
margin-right: 8px;
.mat-subheading-2 {
color: mat-color($mycroft-accent, A700);
font-weight: bold;
margin: 8px;
min-width: 120px;
}
.mat-body {
margin: 0;
min-width: 200px;
}
// button {
// @include action-button-primary;
// margin: 8px;
// }
}
}

View File

@ -0,0 +1,19 @@
import { Component, Input, OnInit } from '@angular/core';
import { Account } from '../profile.service';
@Component({
selector: 'account-agreements',
templateUrl: './agreements.component.html',
styleUrls: ['./agreements.component.scss']
})
export class AgreementsComponent implements OnInit {
@Input() account: Account;
// public termsOfUseAccepted: string;
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,12 @@
<mat-card>
<mat-toolbar>
<span>Delete Account</span>
</mat-toolbar>
<div fxLayout="column" fxLayoutAlign="start center">
<span class="mat-h3">CAUTION</span>
<div id="delete-warning">
<div *ngFor="let paragraph of deleteWarning" class="mat-body-2">{{paragraph}}</div>
</div>
<button mat-raised-button color="warn">DELETE ACCOUNT</button>
</div>
</mat-card>

View File

@ -0,0 +1,25 @@
@import "~src/stylesheets/components/cards";
mat-card {
@include section-card;
button {
color: white;
margin-bottom: 10px;
margin-left: auto;
margin-right: auto;
}
#delete-warning {
margin-bottom: 24px;
.mat-body-2 {
text-align: center;
}
}
.mat-h3 {
text-align: center;
font-weight: bold;
}
}

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'account-delete',
templateUrl: './delete.component.html',
styleUrls: ['./delete.component.scss']
})
export class DeleteComponent implements OnInit {
public deleteWarning: string[];
constructor() { }
ngOnInit() {
this.deleteWarning = [
'Pressing the button below will delete your account and all data related to it from Mycroft servers.',
'It cannot be undone.'
];
}
}

View File

@ -0,0 +1,21 @@
<mat-card>
<mat-toolbar>
<span>Profile</span>
</mat-toolbar>
<mat-card-content fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="space-between center">
<div fxFlex>
<div class="profile-text" fxLayout="row" fxLayoutAlign="start center">
<span class="mat-subheading-2">Email:</span>
<p class="mat-body">{{account.emailAddress}}</p>
</div>
<div fxLayout="row" fxLayoutAlign="start center">
<span class="mat-subheading-2">Username:</span>
<p class="mat-body">{{account.username}}</p>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center">
<button mat-button>CHANGE USERNAME</button>
<button mat-button>CHANGE PASSWORD</button>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,33 @@
@import "~@angular/material/theming";
@import "mycroft-colors";
@import "~src/stylesheets/components/buttons";
@import "~src/stylesheets/components/cards";
mat-card {
@include section-card;
mat-card-content {
margin-left: 16px;
margin-right: 8px;
.mat-subheading-2 {
color: mat-color($mycroft-accent, A700);
font-weight: bold;
margin: 8px;
min-width: 120px;
}
.mat-body {
margin: 0;
min-width: 200px;
}
button {
@include action-button-primary;
margin: 8px;
}
}
}

View File

@ -0,0 +1,17 @@
import { Component, Input } from '@angular/core';
import { faEdit } from '@fortawesome/free-solid-svg-icons';
import { Account } from '../profile.service';
@Component({
selector: 'account-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
@Input() account: Account;
public editIcon = faEdit;
constructor() { }
}

Some files were not shown because too many files have changed in this diff Show More