From 84809ba51bd5ab108d5b3c26423aa66df5218ce5 Mon Sep 17 00:00:00 2001 From: Christian Guy Date: Tue, 2 Apr 2019 10:17:09 -0400 Subject: [PATCH 01/25] V5 build support --- .babelrc | 13 +- .editorconfig | 2 +- .eslintrc | 59 + .eslintrc.js | 43 - .gitignore | 2 +- .jscsrc | 3 + lib/LintHandler.js | 136 +- lib/MessageHandler.js | 294 +- lib/actions/index.js | 283 + lib/epics/index.js | 507 + lib/lint-handler-registry.js | 29 + lib/message-handler-registry.js | 62 + lib/reducers/index.js | 238 + lib/redux-store/configure-store.js | 38 + lib/spl-build-common.js | 1745 ++- lib/spl-build.js | 1149 +- lib/util/keychain-utils.js | 48 + lib/util/rest-v5-response-selector.js | 134 + lib/util/source-archive-utils.js | 205 + lib/util/state-selectors.js | 316 + lib/util/status-utils.js | 159 + lib/util/streams-rest-v5.js | 443 + lib/util/streams-toolkits-utils.js | 182 + lib/util/streams-utils.js | 89 + lib/views/MainCompositePicker.js | 140 +- lib/views/MainCompositePickerView.js | 124 +- .../icp4dAuth/Icp4dAuthenticationView.js | 40 + lib/views/icp4dAuth/components/Step1.js | 288 + lib/views/icp4dAuth/components/Step2.js | 135 + lib/views/icp4dAuth/components/Step3.js | 143 + lib/views/icp4dAuth/components/Wizard.js | 56 + package-lock.json | 11560 ++++++++++++++++ package.json | 68 +- 33 files changed, 17055 insertions(+), 1678 deletions(-) create mode 100644 .eslintrc delete mode 100644 .eslintrc.js create mode 100644 .jscsrc create mode 100644 lib/actions/index.js create mode 100644 lib/epics/index.js create mode 100644 lib/lint-handler-registry.js create mode 100644 lib/message-handler-registry.js create mode 100644 lib/reducers/index.js create mode 100644 lib/redux-store/configure-store.js create mode 100644 lib/util/keychain-utils.js create mode 100644 lib/util/rest-v5-response-selector.js create mode 100644 lib/util/source-archive-utils.js create mode 100644 lib/util/state-selectors.js create mode 100644 lib/util/status-utils.js create mode 100644 lib/util/streams-rest-v5.js create mode 100644 lib/util/streams-toolkits-utils.js create mode 100644 lib/util/streams-utils.js create mode 100644 lib/views/icp4dAuth/Icp4dAuthenticationView.js create mode 100644 lib/views/icp4dAuth/components/Step1.js create mode 100644 lib/views/icp4dAuth/components/Step2.js create mode 100644 lib/views/icp4dAuth/components/Step3.js create mode 100644 lib/views/icp4dAuth/components/Wizard.js create mode 100644 package-lock.json diff --git a/.babelrc b/.babelrc index e9715e4..21cc8c8 100644 --- a/.babelrc +++ b/.babelrc @@ -1,8 +1,9 @@ { - "presets": ["flow", "env", "stage-2"], - "plugins": [ - ["babel-plugin-dynamic-import-node"], - ["dynamic-import-node"] - ], - "sourceMap": "inline" + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "dynamic-import-node" + ] } diff --git a/.editorconfig b/.editorconfig index 7ae77be..70d02e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true [*] charset = utf-8 -indent_style = tab +indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..ba46948 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,59 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "env": { + "es6": true, + "browser": true, + "node": true, + "jquery": true + }, + "rules": { + "array-callback-return": ["off"], + "arrow-body-style": ["off"], + "arrow-parens": ["off"], + "class-methods-use-this": 0, + "compat/compat": 2, + "consistent-return": "off", + "comma-dangle": "off", + "flowtype-errors/show-errors": 2, + "generator-star-spacing": "off", + "import/no-unresolved": ["error", { "ignore": ["electron", "atom"] }], + "import/no-extraneous-dependencies": "off", + "jsx-a11y/no-static-element-interactions": 0, + "jsx-a11y/label-has-associated-control": [ 2, { + "controlComponents": [ "Field" ] + }], + "jsx-a11y/label-has-for": 0, + "max-len": ["off"], + "no-cond-assign": ["error", "except-parens"], + "no-console": 1, + "no-param-reassign": ["error", { "props": false }], + "no-return-assign": ["off"], + "no-use-before-define": "off", + "no-underscore-dangle": "off", + "no-unused-vars": ["error", { "args": "none" }], + "prefer-destructuring": ["error", {"array": false}], + "promise/param-names": 2, + "promise/always-return": 0, + "promise/catch-or-return": 0, + "promise/no-native": 0, + "react/jsx-no-bind": "off", + "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], + "react/no-find-dom-node": 0, + "react/no-string-refs": 0, + "react/prefer-stateless-function": "off", + "react/sort-comp": "off" + }, + "plugins": [ + "flowtype", + "flowtype-errors", + "import", + "promise", + "compat", + "react" + ], + "globals": { + "atom": "readable", + "electron": "readable" + } +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 93f75cd..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - parser: "babel-eslint", - plugins: [ - "babel", - "flowtype" - ], - extends: [ - "plugin:flowtype/recommended" - ], - rules: { - // Disable strict warning on ES6 Components - "strict": 0, - "global-require": 0, - "sort-imports": 0, - //"react/jsx-indent-props": [2, "tab"], - - // Allow class level arrow functions - "no-invalid-this": 0, - "babel/no-invalid-this": 1, - - // Allow flow type annotations on top - // "react/sort-comp": [1, { - // order: [ - // "type-annotations", - // "static-methods", - // "lifecycle", - // "everything-else", - // "render", - // ], - // }], - - // Allow underscore in property names - "camelcase": ["off"], - - // Intent rules - "indent": ["error", "tab", { - "SwitchCase": 1, - "CallExpression": { - "arguments": "off", - }, - }], - } -}; diff --git a/.gitignore b/.gitignore index 754b526..25c51ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ npm-debug.log node_modules .vscode -package-lock.json yarn.lock +toolkitsCache diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..6b2db04 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,3 @@ +{ + "requireTrailingComma": false +} diff --git a/lib/LintHandler.js b/lib/LintHandler.js index cb15e3f..49b55e2 100644 --- a/lib/LintHandler.js +++ b/lib/LintHandler.js @@ -1,78 +1,80 @@ -// @flow -"use strict"; -"use babel"; +'use babel'; +'use strict'; -export class LintHandler { +import StreamsUtils from './util/streams-utils'; - linter = null; - msgRegex = null; - appRoot = null; +export default class LintHandler { + linter = null; - constructor(linter, msgRegex, appRoot) { - this.linter = linter; - this.msgRegex = msgRegex; - this.appRoot = appRoot; - } + msgRegex = null; + appRoot = null; - lint(input) { - if (!this.linter || !input) { - return; - } + constructor(linter, msgRegex, appRoot) { + this.linter = linter; + this.msgRegex = StreamsUtils.SPL_MSG_REGEX; + this.appRoot = appRoot; + } - if (input.output && Array.isArray(input.output)) { - let convertedMessages = input.output.map( - (message) => message.message_text - ).filter( - // filter only messages that match expected format - (msg) => msg.match(this.msgRegex) - ).map( - (msg) => { - // return objects for each message - let parts = msg.match(this.msgRegex); - let severityCode = parts[4].trim().substr(parts[4].trim().length - 1); - let severity = "info"; - if (severityCode) { - switch (severityCode) { - case "I": - severity = "info"; - break; - case "W": - severity = "warning"; - break; - case "E": - severity = "error"; - break; - default: - break; - } - } - let absolutePath = parts[1]; - if (this.appRoot && typeof(this.appRoot) === "string") { - absolutePath = `${this.appRoot}/${parts[1]}`; - } - return { - severity: severity, - location: { + lint(input) { + if (!this.linter || !input) { + return; + } - file: absolutePath, - position: [ - [parseInt(parts[2])-1 ,parseInt(parts[3])-1], - [parseInt(parts[2])-1,parseInt(parts[3])] - ], // 0-indexed - }, - excerpt: parts[4], - description: parts[5], - }; - } - ); + if (input.output && Array.isArray(input.output)) { + const convertedMessages = input.output.map( + (message) => message.message_text + ).filter( + // filter only messages that match expected format + (msg) => msg.match(this.msgRegex) + ).map( + (msg) => { + // return objects for each message + const parts = msg.match(this.msgRegex); + const severityCode = parts[4].trim().substr(parts[4].trim().length - 1); + let severity = 'info'; + if (severityCode) { + switch (severityCode) { + case 'I': + severity = 'info'; + break; + case 'W': + severity = 'warning'; + break; + case 'E': + severity = 'error'; + break; + default: + break; + } + } + let absolutePath = parts[1]; + if (this.appRoot && typeof (this.appRoot) === 'string') { + absolutePath = `${this.appRoot}/${parts[1]}`; + } - this.linter.setAllMessages(convertedMessages); + return { + severity, + location: { - if (Array.isArray(convertedMessages) && convertedMessages.length > 0) { - atom.workspace.open("atom://nuclide/diagnostics"); - } - } - } + file: absolutePath, + position: [ + [parseInt(parts[2], 10) - 1, parseInt(parts[3], 10) - 1], + [parseInt(parts[2], 10) - 1, parseInt(parts[3], 10)] + ], // 0-indexed + }, + excerpt: parts[4], + description: parts[5], + }; + } + ); + + this.linter.setAllMessages(convertedMessages); + + if (Array.isArray(convertedMessages) && convertedMessages.length > 0) { + atom.workspace.open('atom://nuclide/diagnostics'); + } + } + } } diff --git a/lib/MessageHandler.js b/lib/MessageHandler.js index 9e270ac..3915e71 100644 --- a/lib/MessageHandler.js +++ b/lib/MessageHandler.js @@ -1,153 +1,171 @@ -// @flow +'use babel'; +'use strict'; -"use strict"; -"use babel"; +export default class MessageHandler { + consoleService: null; -export class MessageHandler { - consoleService: null; + constructor(service) { + this.consoleService = service; + } - constructor(service) { - this.consoleService = service; - } + handleInfo( + message, + { + detail = null, + description = null, + showNotification = true, + showConsoleMessage = true, + notificationAutoDismiss = true, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); - handleInfo( - message, - { - detail = null, - description = null, - showNotification = true, - showConsoleMessage = true, - notificationAutoDismiss = true, - notificationButtons = [] - } = {} - ) { - const addedButtons = this.processButtons(notificationButtons); - const detailMessage = this.joinMessageArray(detail); + if (showConsoleMessage) { + this.consoleService.log(`${message}${detailMessage ? `\n${detailMessage}` : ''}`); + } + if (showNotification && typeof (message) === 'string') { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage || '', + description: description || '' + }; + return atom.notifications.addInfo(message, notificationOptions); + } + } - if (showConsoleMessage) { - this.consoleService.log(`${message}${detailMessage ? "\n"+detailMessage: ""}`); - } - if (showNotification && typeof(message) === "string") { - const notificationOptions = { - ...addedButtons, - dismissable: !notificationAutoDismiss, - detail: detailMessage ? detailMessage : "", - description: description ? description : "" - }; - return atom.notifications.addInfo(message, notificationOptions); - } - } + handleError( + message, + { + detail, + description, + stack, + showNotification = true, + showConsoleMessage = true, + consoleErrorLog = true, + notificationAutoDismiss = false, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); + const stackMessage = this.joinMessageArray(stack); - handleError( - message, - { - detail, - description, - stack, - showNotification = true, - showConsoleMessage = true, - consoleErrorLog = true, - notificationAutoDismiss = false, - notificationButtons = [] - } = {} - ) { - const addedButtons = this.processButtons(notificationButtons); - const detailMessage = this.joinMessageArray(detail); - const stackMessage = this.joinMessageArray(stack); + if (consoleErrorLog) { + if (stack) { + console.error(message, stack); + } else { + console.error(message); + } + } + if (showConsoleMessage) { + this.consoleService.error(message); + if (typeof (detailMessage) === 'string' && detailMessage.length > 0) { + this.consoleService.error(detailMessage); + } + } + if (showNotification && typeof (message) === 'string') { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage || '', + stack: stackMessage || '', + description: description || '' + }; + return atom.notifications.addError(message, notificationOptions); + } + } - if (consoleErrorLog) { - if (stack) { - console.error(message, stack); - } else { - console.error(message); - } - } - if (showConsoleMessage) { - this.consoleService.error(message); - if (typeof(detailMessage) === "string" && detailMessage.length > 0) { - this.consoleService.error(detailMessage); - } - } - if (showNotification && typeof(message) === "string") { - const notificationOptions = { - ...addedButtons, - dismissable: !notificationAutoDismiss, - detail: detailMessage ? detailMessage : "", - stack: stackMessage ? stackMessage: "", - description: description ? description : "" - }; - return atom.notifications.addError(message, notificationOptions); - } - } + handleSuccess( + message, + { + detail = null, + description = null, + showNotification = true, + showConsoleMessage = true, + notificationAutoDismiss = false, + notificationButtons = [] + } = {} + ) { + const addedButtons = this.processButtons(notificationButtons); + const detailMessage = this.joinMessageArray(detail); - handleSuccess( - message, - { - detail = null, - description = null, - showNotification = true, - showConsoleMessage = true, - notificationAutoDismiss = false, - notificationButtons = [] - } = {} - ) { - const addedButtons = this.processButtons(notificationButtons); - const detailMessage = this.joinMessageArray(detail); + if (showConsoleMessage) { + this.consoleService.log(`${message}${detailMessage ? `\n${detailMessage}` : ''}`); + } + if (showNotification && typeof (message) === 'string') { + const notificationOptions = { + ...addedButtons, + dismissable: !notificationAutoDismiss, + detail: detailMessage || '', + description: description || '' + }; + return atom.notifications.addSuccess(message, notificationOptions); + } + } - if (showConsoleMessage) { - this.consoleService.log(`${message}${detailMessage ? "\n"+detailMessage: ""}`); - } - if (showNotification && typeof(message) === "string") { - const notificationOptions = { - ...addedButtons, - dismissable: !notificationAutoDismiss, - detail: detailMessage ? detailMessage : "", - description: description ? description : "" - }; - return atom.notifications.addSuccess(message, notificationOptions); - } - } + handleCredentialsMissing(errorNotification) { + const n = atom.notifications.addError( + 'Copy and paste the Streaming Analytics service credentials into the build-ibmstreams package settings page.', + { + dismissable: true, + buttons: [{ + text: 'Open package settings', + onDidClick: () => { + this.dismissNotification(errorNotification); + this.dismissNotification(n); + this.openPackageSettingsPage(); + } + }] + } + ); + return n; + } - handleCredentialsMissing(errorNotification) { - const n = atom.notifications.addError( - "Copy and paste the Streaming Analytics service credentials into the build-ibmstreams package settings page.", - { - dismissable: true, - buttons: [{ - text: "Open package settings", - onDidClick: () => { - this.dismissNotification(errorNotification); - this.dismissNotification(n); - atom.workspace.open("atom://config/packages/build-ibmstreams"); - } - }] - } - ); - return n; - } + handleIcp4dUrlNotSet() { + const notification = this.handleError('IBM Cloud Private for Data URL not specified', + { + detail: 'Specify the IBM Cloud Private for Data URL or build with IBM Cloud Streaming Analytics in the build-ibmstreams package settings.', + notificationButtons: [ + { + label: 'Open settings', + callbackFn: () => { + this.openPackageSettingsPage(); + notification.dismiss(); + } + } + ] + }); + } - processButtons(btns) { - let buttons = {}; - if (Array.isArray(btns)) { - buttons.buttons = btns.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); - } - return buttons; - } + openPackageSettingsPage() { + atom.workspace.open('atom://config/packages/build-ibmstreams'); + } - joinMessageArray(msgArray) { - if (Array.isArray(msgArray)) { - return msgArray.join("\n").trimRight(); - } - return msgArray; - } + processButtons(btns) { + const buttons = {}; + if (Array.isArray(btns)) { + buttons.buttons = btns.map(obj => ({ onDidClick: obj.callbackFn, text: obj.label })); + } + return buttons; + } - dismissNotification(notification) { - if (notification && typeof(notification.dismiss) === "function") { - notification.dismiss(); - } - } + joinMessageArray(msgArray) { + if (Array.isArray(msgArray)) { + return msgArray.join('\n').trimRight(); + } + return msgArray; + } - getLoggableMessage(messages: Array) { - return this.joinMessageArray(messages.map(outputMsg => outputMsg.message_text)); - } + dismissNotification(notification) { + if (notification && typeof (notification.dismiss) === 'function') { + notification.dismiss(); + } + } + + getLoggableMessage(messages: Array) { + return this.joinMessageArray(messages.map(outputMsg => outputMsg.message_text)); + } } diff --git a/lib/actions/index.js b/lib/actions/index.js new file mode 100644 index 0000000..f506dac --- /dev/null +++ b/lib/actions/index.js @@ -0,0 +1,283 @@ +'use babel'; +'use strict'; + +/* eslint-disable import/prefer-default-export */ + +export const setIcp4dUrl = (icp4dUrl) => ({ + type: actions.SET_ICP4D_URL, + icp4dUrl +}); + +export const setUseIcp4dMasterNodeHost = (useIcp4dMasterNodeHost) => ({ + type: actions.SET_USE_ICP4D_MASTER_NODE_HOST, + useIcp4dMasterNodeHost +}); + +export const setCurrentLoginStep = (step) => ({ + type: actions.SET_CURRENT_LOGIN_STEP, + currentLoginStep: step +}); + +export const setUsername = (username) => ({ + type: actions.SET_USERNAME, + username +}); + +export const setPassword = (password) => ({ + type: actions.SET_PASSWORD, + password +}); + +export const setRememberPassword = (rememberPassword) => ({ + type: actions.SET_REMEMBER_PASSWORD, + rememberPassword +}); + +export const setFormDataField = (key, value) => ({ + type: actions.SET_FORM_DATA_FIELD, + key, + value +}); + +export const setBuildOriginator = (originator, version) => ({ + type: actions.SET_BUILD_ORIGINATOR, + originator, + version +}); + +export const queueAction = (queuedAction) => ({ + type: actions.QUEUE_ACTION, + queuedAction +}); + +export const clearQueuedAction = () => ({ + type: actions.CLEAR_QUEUED_ACTION +}); + +export const authenticateIcp4d = (username, password, rememberPassword) => ({ + type: actions.AUTHENTICATE_ICP4D, + username, + password, + rememberPassword +}); + +export const authenticateStreamsInstance = (instanceName) => ({ + type: actions.AUTHENTICATE_STREAMS_INSTANCE, + instanceName +}); + +export const setStreamsInstances = (streamsInstances) => ({ + type: actions.SET_STREAMS_INSTANCES, + streamsInstances +}); + +export const setSelectedInstance = (streamsInstance) => ({ + type: actions.SET_SELECTED_INSTANCE, + ...streamsInstance, + currentLoginStep: 3 +}); + +export const setIcp4dAuthToken = (authToken) => ({ + type: actions.SET_ICP4D_AUTH_TOKEN, + authToken, + currentLoginStep: 2 +}); + +export const setIcp4dAuthError = (authError) => ({ + type: actions.SET_ICP4D_AUTH_ERROR, + authError +}); + +export const setStreamsAuthToken = (authToken) => ({ + type: actions.SET_STREAMS_AUTH_TOKEN, + authToken +}); + +export const setStreamsAuthError = (authError) => ({ + type: actions.SET_STREAMS_AUTH_ERROR, + authError +}); + +export const startBuild = (buildId) => ({ + type: actions.START_BUILD, + buildId +}); + +export const newBuild = ({ + appRoot, + toolkitRootPath, + fqn, + makefilePath, + postBuildAction +}) => ({ + type: actions.NEW_BUILD, + appRoot, + toolkitRootPath, + fqn, + makefilePath, + postBuildAction +}); + +export const uploadSource = ( + buildId, + appRoot, + toolkitRootPath, + fqn, + makefilePath +) => ({ + type: actions.BUILD_UPLOAD_SOURCE, + buildId, + appRoot, + toolkitRootPath, + fqn, + makefilePath +}); + +export const getBuildStatus = (buildId) => ({ + type: actions.GET_BUILD_STATUS, + buildId +}); + +export const logBuildStatus = (buildId) => ({ + type: actions.LOG_BUILD_STATUS, + buildId +}); + +export const getBuildStatusFulfilled = (buildStatusResponse) => ({ + type: actions.GET_BUILD_STATUS_FULFILLED, + ...buildStatusResponse +}); + +export const getBuildLogMessagesFulfilled = (buildLogMessagesResponse) => ({ + type: actions.GET_BUILD_LOG_MESSAGES_FULFILLED, + ...buildLogMessagesResponse +}); + +export const buildSucceeded = (buildId) => ({ + type: actions.BUILD_SUCCESS, + buildId +}); + +export const buildFailed = (buildId) => ({ + type: actions.BUILD_FAILED, + buildId +}); + +export const buildInProgress = (buildId) => ({ + type: actions.BUILD_IN_PROGRESS, + buildId +}); + +export const buildStatusReceived = (buildId) => ({ + type: actions.BUILD_STATUS_RECEIVED, + buildId +}); + +export const getBuildArtifacts = (buildId) => ({ + type: actions.GET_BUILD_ARTIFACTS, + buildId +}); + +export const getBuildArtifactsFulfilled = (buildId, artifacts) => ({ + type: actions.GET_BUILD_ARTIFACTS_FULFILLED, + buildId, + artifacts +}); + +export const downloadAppBundles = (buildId) => ({ + type: actions.DOWNLOAD_APP_BUNDLES, + buildId +}); + +export const submitApplications = (buildId, fromArtifact) => ({ + type: actions.SUBMIT_APPLICATIONS, + buildId, + fromArtifact +}); + +export const submitApplicationsFromBundleFiles = (bundles) => ({ + type: actions.SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES, + bundles +}); + +export const openStreamingAnalyticsConsole = () => ({ + type: actions.OPEN_STREAMS_CONSOLE +}); + +export const refreshToolkits = () => ({ + type: actions.REFRESH_TOOLKITS +}); + +export const setToolkitsCacheDir = (toolkitsCacheDir) => ({ + type: actions.SET_TOOLKITS_CACHE_DIR, + toolkitsCacheDir +}); + +export const setToolkitsPathSetting = (toolkitsPathSetting) => ({ + type: actions.SET_TOOLKITS_PATH_SETTING, + toolkitsPathSetting +}); + +export const handleError = (sourceAction, error) => ({ + type: actions.ERROR, + sourceAction, + error +}); + +export const actions = { + PACKAGE_ACTIVATED: 'PACKAGE_ACTIVATED', + LOGIN_FORM_INITIALIZED: 'LOGIN_FORM_INITIALIZED', + SET_BUILD_ORIGINATOR: 'SET_BUILD_ORIGINATOR', + ERROR: 'ERROR', + SET_CURRENT_LOGIN_STEP: 'SET_CURRENT_LOGIN_STEP', + SET_ICP4D_URL: 'SET_ICP4D_URL', + SET_USE_ICP4D_MASTER_NODE_HOST: 'SET_USE_ICP4D_MASTER_NODE_HOST', + SET_USERNAME: 'SET_USERNAME', + SET_PASSWORD: 'SET_PASSWORD', + SET_REMEMBER_PASSWORD: 'SET_REMEMBER_PASSWORD', + SET_FORM_DATA_FIELD: 'SET_FORM_DATA_FIELD', + AUTHENTICATE_ICP4D: 'AUTHENTICATE_ICP4D', + AUTHENTICATE_STREAMS_INSTANCE: 'AUTHENTICATE_STREAMS_INSTANCE', + SET_ICP4D_AUTH_TOKEN: 'SET_ICP4D_AUTH_TOKEN', + SET_ICP4D_AUTH_ERROR: 'SET_ICP4D_AUTH_ERROR', + SET_SELECTED_INSTANCE: 'SET_SELECTED_INSTANCE', + SET_STREAMS_AUTH_TOKEN: 'SET_STREAMS_AUTH_TOKEN', + SET_STREAMS_AUTH_ERROR: 'SET_STREAMS_AUTH_ERROR', + SET_STREAMS_INSTANCES: 'SET_STREAMS_INSTANCES', + + QUEUE_ACTION: 'QUEUE_ACTION', + CLEAR_QUEUED_ACTION: 'CLEAR_QUEUED_ACTION', + + NEW_BUILD: 'NEW_BUILD', + START_BUILD: 'START_BUILD', + CANCEL_BUILD: 'CANCEL_BUILD', + BUILD_UPLOAD_SOURCE: 'BUILD_UPLOAD_SOURCE', + SOURCE_ARCHIVE_CREATED: 'SOURCE_ARCHIVE_CREATED', + + GET_BUILD_STATUS: 'GET_BUILD_STATUS', + GET_BUILD_STATUS_FULFILLED: 'GET_BUILD_STATUS_FULFILLED', + GET_BUILD_LOG_MESSAGES: 'GET_BUILD_LOG_MESSAGES', + GET_BUILD_LOG_MESSAGES_FULFILLED: 'GET_BUILD_LOG_MESSAGES_FULFILLED', + LOG_BUILD_STATUS: 'LOG_BUILD_STATUS', + BUILD_SUCCESS: 'BUILD_SUCCESS', + BUILD_FAILED: 'BUILD_FAILED', + BUILD_IN_PROGRESS: 'BUILD_IN_PROGRESS', + BUILD_STATUS_RECEIVED: 'BUILD_STATUS_RECEIVED', + + GET_BUILD_ARTIFACTS: 'GET_BUILD_ARTIFACTS', + GET_BUILD_ARTIFACTS_FULFILLED: 'GET_BUILD_ARTIFACTS_FULFILLED', + + DOWNLOAD_APP_BUNDLES: 'DOWNLOAD_APP_BUNDLES', + SUBMIT_APPLICATIONS: 'SUBMIT_APPLICATIONS', + SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES: 'SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES', + + REFRESH_TOOLKITS: 'REFRESH_TOOLKITS', + SET_TOOLKITS_CACHE_DIR: 'SET_TOOLKITS_CACHE_DIR', + SET_TOOLKITS_PATH_SETTING: 'SET_TOOLKITS_PATH_SETTING', + + OPEN_STREAMS_CONSOLE: 'OPEN_STREAMS_CONSOLE', + OPEN_ICP4D_CONSOLE: 'OPEN_ICP4D_CONSOLE', + + REFRESH_AUTH_TOKEN: 'REFRESH_AUTH_TOKEN', + POST_AUTH: 'POST_AUTH' +}; diff --git a/lib/epics/index.js b/lib/epics/index.js new file mode 100644 index 0000000..d3a1638 --- /dev/null +++ b/lib/epics/index.js @@ -0,0 +1,507 @@ +'use babel'; +'use strict'; + +import { combineEpics, ofType } from 'redux-observable'; +import * as path from 'path'; +import * as fs from 'fs'; +import { + defaultIfEmpty, + map, + mergeMap, + tap, + delay, + withLatestFrom, + catchError, +} from 'rxjs/operators'; +import { + from, + zip, + of, + empty, + merge, + forkJoin +} from 'rxjs'; + +import { + actions, + + handleError, + + authenticateIcp4d, + authenticateStreamsInstance, + + clearQueuedAction, + + setStreamsInstances, + setIcp4dAuthToken, + setIcp4dAuthError, + setStreamsAuthToken, + setStreamsAuthError, + + getBuildArtifacts, + getBuildArtifactsFulfilled, + + uploadSource, + getBuildStatus, + getBuildStatusFulfilled, + getBuildLogMessagesFulfilled, + startBuild, + buildStatusReceived, + + refreshToolkits, + setFormDataField, +} from '../actions'; +import StateSelector from '../util/state-selectors'; +import ResponseSelector from '../util/rest-v5-response-selector'; +import StreamsRestUtils from '../util/streams-rest-v5'; +import SourceArchiveUtils from '../util/source-archive-utils'; +import StatusUtils from '../util/status-utils'; +import StreamsToolkitUtils from '../util/streams-toolkits-utils'; +import KeychainUtils from '../util/keychain-utils'; +import MessageHandlerRegistry from '../message-handler-registry'; + + +/** + * Consumes a NEW_BUILD action, creates a new build in the build service, + * and emits a BUILD_UPLOAD_SOURCE action. + * @param {*} action + * @param {*} state + */ +const buildAppEpic = (action, state) => action.pipe( + ofType(actions.NEW_BUILD), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.build.create(s, { originator: 'atom', name: action.sourceArchive }).pipe( + map(createBuildResponse => { + const buildId = ResponseSelector.getBuildId(createBuildResponse); + if (!buildId) { + throw new Error('Unable to retrieve build id'); + } + const newBuild = StateSelector.getNewBuild(s, buildId); + return uploadSource( + buildId, + newBuild.appRoot, + newBuild.toolkitRootPath, + newBuild.fqn, + newBuild.makefilePath + ); + }), + catchError(error => of(handleError(a, error))) + )), +); + +/** + * Consumes a BUILD_UPLOAD_SOURCE action, generates source archive zip file, + * and emits a SOURCE_ARCHIVE_CREATED action. + * @param {*} action + * @param {*} state + */ +const uploadSourceEpic = (action, state) => action.pipe( + ofType(actions.BUILD_UPLOAD_SOURCE), + mergeMap(uploadAction => SourceArchiveUtils.buildSourceArchive({ + buildId: uploadAction.buildId, + appRoot: uploadAction.appRoot, + toolkitRootPath: uploadAction.toolkitRootPath, + fqn: uploadAction.fqn, + makefilePath: uploadAction.makefilePath + })), + catchError(error => of(handleError(action, error))) +); + +/** + * Consumes SOURCE_ARCHIVE_CREATED action, uploads source archive to build service, + * and emits a START_BUILD action + * @param {*} action + * @param {*} state + */ +const sourceArchiveCreatedEpic = (action, state) => { + return action.pipe( + ofType(actions.SOURCE_ARCHIVE_CREATED), + withLatestFrom(state), + mergeMap(([sourceArchiveResponse, s]) => StreamsRestUtils.build.uploadSource(s, sourceArchiveResponse.buildId, sourceArchiveResponse.archivePath).pipe( + map((a) => startBuild(sourceArchiveResponse.buildId)), + tap(() => { + if (fs.existsSync(sourceArchiveResponse.archivePath)) { + fs.unlinkSync(sourceArchiveResponse.archivePath); + } + }), + catchError(error => of(handleError(sourceArchiveResponse, error))) + )), + ); +}; + +/** + * Consumes START_BUILD action, + * starts the build and emits an GET_BUILD_STATUS action + * @param {*} action + * @param {*} state + */ +const startBuildEpic = (action, state) => { + return action.pipe( + ofType(actions.START_BUILD), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.build.start(s, a.buildId).pipe( + delay(1000), + map(() => getBuildStatus(a.buildId)), + catchError(error => of(handleError(a, error))) + )), + ); +}; + +/** + * This epic handles requests for build status updates, + * fetches build status and build log messages for action.buildId; + * waits for get build status and build log messages actions to complete + * and emits set of 3 actions, + * BUILD_STATUS_FULFILLED, BUILD_LOG_FULFILLED, and BUILD_STATUS_RECEIVED. + * FULFILLED actions update the state, RECEIVED action handles UI updates + * and build status check loop. + * @param {*} action + */ +const buildStatusEpic = (action, state) => action.pipe( + ofType(actions.GET_BUILD_STATUS), + withLatestFrom(state), + // get build status and build message log and wait for both to complete, + // passes on [BUILD_STATUS_FULFILLED, BUILD_LOG_FULFILLED] + mergeMap(([a, s]) => zip( + StreamsRestUtils.build.getStatus(s, a.buildId).pipe( + map(response => getBuildStatusFulfilled(ResponseSelector.getBuildStatus(response))), + catchError(error => of(handleError(a, error))) + ), + StreamsRestUtils.build.getLogMessages(s, a.buildId).pipe( + tap(r => console.log('log response: ', r)), + map(response => getBuildLogMessagesFulfilled({ buildId: a.buildId, logMessages: response.body.split('\n') })), + catchError(error => of(handleError(a, error))) + ) + )), + // emit [BUILD_STATUS_FULFILLED, BUILD_LOG_FULFILLED, BUILD_STATUS_RECEIVED] + mergeMap(([statusFulfilledAction, logFulfilledAction]) => [statusFulfilledAction, logFulfilledAction, buildStatusReceived(statusFulfilledAction.buildId)]) +); + +const buildStatusLoopEpic = (action, state) => action.pipe( + ofType(actions.BUILD_STATUS_RECEIVED), + withLatestFrom(state), + tap(([a, s]) => { + // message handling for updated build status + StatusUtils.buildStatusUpdate(a, s); + }), + mergeMap(([a, s]) => merge( + shouldGetBuildStatusHelperObservable(a, s).pipe( + map(() => getBuildStatus(a.buildId)), + catchError(error => of(handleError(a, error))) + ), + shouldGetArtifactsHelperObservable(a, s).pipe( + map(() => getBuildArtifacts(a.buildId)), + catchError(error => of(handleError(a, error))) + ) + )), +); + +const shouldGetBuildStatusHelperObservable = (action, state) => { + const buildStatus = StateSelector.getBuildStatus(state, action.buildId); + if (buildStatus === 'building' || buildStatus === 'created' || buildStatus === 'waiting') { + return of(1).pipe( + delay(5000), + ); + } + return empty(); +}; + +const shouldGetArtifactsHelperObservable = (action, state) => { + const buildStatus = StateSelector.getBuildStatus(state, action.buildId); + if (buildStatus === 'built') { + return of(1); + } + return empty(); +}; + +const getBuildArtifactsEpic = (action, state) => action.pipe( + ofType(actions.GET_BUILD_ARTIFACTS), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.artifact.getArtifacts(s, a.buildId).pipe( + map(artifactResponse => getBuildArtifactsFulfilled(a.buildId, ResponseSelector.getBuildArtifacts(artifactResponse))), + catchError(error => of(handleError(a, error))) + )) +); + +const getBuildArtifactsFulfilledEpic = (action, state) => action.pipe( + ofType(actions.GET_BUILD_ARTIFACTS_FULFILLED), + withLatestFrom(state), + tap(([a, s]) => { + const { buildId } = a; + StatusUtils.downloadOrSubmit(s, buildId); + }), + map(() => ({ type: 'dummy_action' })) +); + +const downloadArtifactsEpic = (action, state) => action.pipe( + ofType(actions.DOWNLOAD_APP_BUNDLES), + withLatestFrom(state), + mergeMap(([a, s]) => { + const { buildId } = a; + const artifacts = StateSelector.getBuildArtifacts(s, buildId); + return from(artifacts).pipe( + mergeMap(artifact => StreamsRestUtils.artifact.downloadApplicationBundle(s, buildId, artifact.id).pipe( + map(downloadResponse => { + const artifactId = artifact.id; + const artifactOutputPath = StateSelector.getOutputArtifactFilePath(s, buildId, artifactId); + try { + if (fs.existsSync(artifactOutputPath)) { + fs.unlinkSync(artifactOutputPath); + } + const outputDir = path.dirname(artifactOutputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + fs.writeFileSync(artifactOutputPath, downloadResponse.body); + StatusUtils.appBundleDownloaded(s, buildId, artifact.name, artifactOutputPath); + } catch (err) { + console.error(err); + } + }) + )), + map(() => ({ type: 'BUILD_ARTIFACT_DOWNLOADED' })), + catchError(error => of(handleError(a, error))) + ); + }), +); + +const submitApplicationsEpic = (action, state) => action.pipe( + ofType(actions.SUBMIT_APPLICATIONS), + withLatestFrom(state), + mergeMap(([a, s]) => { + const { buildId } = a; + const artifacts = StateSelector.getBuildArtifacts(s, buildId); + return from(artifacts).pipe( + tap(submitArtifact => StatusUtils.submitJobStart(s, submitArtifact.name, buildId)), + mergeMap(artifact => StreamsRestUtils.artifact.submitJob( + s, + artifact.applicationBundle, + {} + ).pipe( + tap(submitResponse => { + const submitInfo = ResponseSelector.getSubmitInfo(submitResponse); + StatusUtils.jobSubmitted(s, submitInfo, buildId); + console.log('submitResponse:', submitResponse); + }) + )), + tap(a1 => { + console.log('downloadArtifacts post-tap', a1); + }), + map(() => ({ type: 'APPLICATION_SUBMITTED' })), + catchError(error => of(handleError(a, error))) + ); + }), +); + +const submitApplicationsFromBundleFilesEpic = (action, state) => action.pipe( + ofType(actions.SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES), + withLatestFrom(state), + mergeMap(([a, s]) => { + return from(a.bundles).pipe( + tap((bundleToUpload) => { + StatusUtils.submitJobStart(s, path.basename(bundleToUpload.bundlePath)); + }), + mergeMap(bundle => StreamsRestUtils.artifact.uploadApplicationBundleToInstance(s, bundle.bundlePath).pipe( + mergeMap(uploadBundleResponse => { + const submitBundleId = ResponseSelector.getUploadedBundleId(uploadBundleResponse); + return StreamsRestUtils.artifact.submitJob(s, submitBundleId, {}).pipe( + tap(submitResponse => { + const submitInfo = ResponseSelector.getSubmitInfo(submitResponse); + StatusUtils.jobSubmitted(s, submitInfo); + console.log('submitResponse:', submitResponse); + }), + map(() => ({ type: 'APPLICATION_SUBMITTED' })) + ); + }) + )), + catchError(error => of(handleError(a, error))) + ); + }), +); + +const openStreamsConsoleEpic = (action, state) => action.pipe( + ofType(actions.OPEN_STREAMS_CONSOLE), + withLatestFrom(state), + tap(([a, s]) => { + MessageHandlerRegistry.openUrl(StateSelector.getStreamsConsoleUrl(s)); + }), + map(() => ({ type: 'STREAMS_CONSOLE_OPENED' })) +); + +const icp4dAuthEpic = (action, state) => action.pipe( + ofType(actions.AUTHENTICATE_ICP4D), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.icp4d.getIcp4dToken(s, a.username, a.password).pipe( + tap(a1 => console.log('icp4d auth response: ', a1)), + mergeMap(authTokenResponse => { + const statusCode = ResponseSelector.getStatusCode(authTokenResponse); + if (statusCode === 200) { + if (a.rememberPassword) { + KeychainUtils.addCredentials(a.username, a.password); + } else { + KeychainUtils.deleteCredentials(a.username); + } + return merge( + of(setIcp4dAuthToken(ResponseSelector.getIcp4dAuthToken(authTokenResponse))), + of(setIcp4dAuthError(false)), + authDelayObservable().pipe( + tap(() => { + console.log('reauthenticating to icp4d'); + }), + mergeMap(() => of(authenticateIcp4d(a.username, a.password, a.rememberPassword))), + catchError(error => of(handleError(a, error))) + ), + ); + } + return of(setIcp4dAuthError(statusCode)); + }), + catchError(error => of(handleError(a, error))) + )) +); + +const authDelayObservable = () => { + return of(1).pipe( + delay(19.5 * 60 * 1000) // icp4d auth tokens expire after 20 minutes + ); +}; + +const streamsAuthEpic = (action, state) => action.pipe( + ofType(actions.AUTHENTICATE_STREAMS_INSTANCE), + withLatestFrom(state), + mergeMap(([authAction, s]) => StreamsRestUtils.icp4d.getStreamsAuthToken(s, authAction.instanceName).pipe( + tap(() => { + console.log('Queued action:', StateSelector.getQueuedAction(s)); + }), + mergeMap(authTokenResponse => { + const statusCode = ResponseSelector.getStatusCode(authTokenResponse); + if (statusCode === 200) { + return merge( + of(setStreamsAuthToken(ResponseSelector.getStreamsAuthToken(authTokenResponse))), + of(setStreamsAuthError(false)), + of(refreshToolkits()), + of(StateSelector.getQueuedAction(s)), // if there was a queued action, run it now... + of(clearQueuedAction()), + authDelayObservable().pipe( + tap(() => { + console.log('reauthenticating to streams instance'); + }), + mergeMap(() => of(authenticateStreamsInstance(authAction.instanceName))), + catchError(error => of(handleError(authAction, error))) + ) + ); + } + return of(setStreamsAuthError(true)); + }), + // merge( + // of(setStreamsAuthToken(ResponseSelector.getStreamsAuthToken(authTokenResponse))), + // of(refreshToolkits()), + // of(StateSelector.getQueuedAction(s)), // if there was a queued action, run it now... + // of(clearQueuedAction()), + // authDelayObservable().pipe( + // mergeMap(() => of(authenticateStreamsInstance(authAction.instanceName))), + // catchError(error => of(handleError(authAction, error))) + // ), + // )), + catchError(error => of(handleError(authAction, error))) + )), +); + +const getStreamsInstancesEpic = (action, state) => action.pipe( + ofType(actions.SET_ICP4D_AUTH_TOKEN), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.icp4d.getServiceInstances(s).pipe( + map(serviceInstancesResponse => ResponseSelector.getStreamsInstances(serviceInstancesResponse)), + map(streamsInstances => setStreamsInstances(streamsInstances)), + catchError(error => of(handleError(a, error))) + )), +); + +const instanceSelectedEpic = (action, state) => action.pipe( + ofType(actions.SET_SELECTED_INSTANCE), + withLatestFrom(state), + map(([a, s]) => authenticateStreamsInstance(StateSelector.getSelectedInstanceName(s))) +); + +const refreshToolkitsEpic = (action, state) => action.pipe( + ofType(actions.REFRESH_TOOLKITS), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.toolkit.getToolkits(s).pipe( + tap(() => MessageHandlerRegistry.getDefault().handleInfo('Initializing toolkit index cache')), + map(toolkitsResponse => ResponseSelector.getToolkits(toolkitsResponse)), + map(toolkits => StreamsToolkitUtils.getToolkitsToCache(s, toolkits)), + tap(a1 => console.log('refreshTK,a', a1)), + mergeMap(toolkitsToCache => forkJoin(from(toolkitsToCache).pipe( + tap(a1 => console.log('refreshTK,b', a1)), + mergeMap(toolkitToCache => StreamsRestUtils.toolkit.getToolkitIndex(s, toolkitToCache.id).pipe( + tap(a1 => console.log('refreshTK,c', a1)), + map(toolkitIndexResponse => StreamsToolkitUtils.cacheToolkitIndex(s, toolkitToCache, toolkitIndexResponse.body)) + )), + defaultIfEmpty('empty') + ))), + tap(() => StreamsToolkitUtils.refreshLspToolkits(s, MessageHandlerRegistry.sendLspNotification)), + map(() => ({ type: 'TOOLKITS_CACHED' })), + tap(() => MessageHandlerRegistry.getDefault().handleSuccess('Toolkit indexes cached successfully', { notificationAutoDismiss: true })), + catchError(error => of(handleError(a, error))) + )) +); + +const packageActivatedEpic = (action, state) => action.pipe( + ofType(actions.PACKAGE_ACTIVATED), + withLatestFrom(state), + map(([a, s]) => { + const username = StateSelector.getUsername(s); + const rememberPassword = StateSelector.getRememberPassword(s); + if (username && rememberPassword) { + const password = KeychainUtils.getCredentials(username); + if (password) { + return setFormDataField('password', password); + } + } + return { type: 'DUMMY_PACKAGE_ACTIVATED_END' }; + }) +); + +const errorHandlingEpic = (action, state) => action.pipe( + ofType(actions.ERROR), + withLatestFrom(state), + tap(([a, s]) => { + console.error('error occurred in action: ', a.sourceAction.type, '\nerror: ', a.error); + if (typeof a.error === 'string') { + MessageHandlerRegistry.getDefault().handleError(a.error, { detail: a.sourceAction.type }); + } else if (a.error) { + MessageHandlerRegistry.getDefault().handleError(a.error.message, { detail: `${a.sourceAction.type}\n\n${a.error.stack}` }); + } + }), + map(() => ({ type: 'ERROR_HANDLED' })) +); + +const rootEpic = combineEpics( + errorHandlingEpic, + + buildAppEpic, + buildStatusEpic, + uploadSourceEpic, + sourceArchiveCreatedEpic, + startBuildEpic, + buildStatusLoopEpic, + + getBuildArtifactsEpic, + getBuildArtifactsFulfilledEpic, + downloadArtifactsEpic, + submitApplicationsEpic, + submitApplicationsFromBundleFilesEpic, + + openStreamsConsoleEpic, + + instanceSelectedEpic, + icp4dAuthEpic, + streamsAuthEpic, + getStreamsInstancesEpic, + + packageActivatedEpic, + + refreshToolkitsEpic, + +); + +export default rootEpic; diff --git a/lib/lint-handler-registry.js b/lib/lint-handler-registry.js new file mode 100644 index 0000000..bdcbfc5 --- /dev/null +++ b/lib/lint-handler-registry.js @@ -0,0 +1,29 @@ +'use babel'; +'use strict'; + +const lintHandlerRegistry = {}; + +function add(identifier, lintHandler) { + lintHandlerRegistry[identifier] = lintHandler; +} + +function remove(identifier) { + lintHandlerRegistry[identifier] = null; +} + +function get(identifier) { + return lintHandlerRegistry[identifier]; +} + +function dispose() { + Object.keys(lintHandlerRegistry).forEach(k => lintHandlerRegistry[k] = null); +} + +const LintHandlerRegistry = { + add, + remove, + get, + dispose +}; + +export default LintHandlerRegistry; diff --git a/lib/message-handler-registry.js b/lib/message-handler-registry.js new file mode 100644 index 0000000..2de7d76 --- /dev/null +++ b/lib/message-handler-registry.js @@ -0,0 +1,62 @@ +'use babel'; +'use strict'; + +const messageHandlerRegistry = {}; +const openUrlHandler = {}; +const sendLspNotificationHandler = {}; + +function add(identifier, messageHandler) { + messageHandlerRegistry[identifier] = messageHandler; +} + +function remove(identifier) { + messageHandlerRegistry[identifier] = null; +} + +function get(identifier) { + return messageHandlerRegistry[identifier]; +} + +function setDefault(messageHandler) { + messageHandlerRegistry.___default = messageHandler; +} + +function getDefault() { + return messageHandlerRegistry.___default; +} + +function setOpenUrlHandler(handler) { + openUrlHandler.___default = handler; +} +function openUrl(url) { + openUrlHandler.___default(url); +} + +function sendLspNotification(param) { + sendLspNotificationHandler.___default(param); +} + +function setSendLspNotificationHandler(handler) { + sendLspNotificationHandler.___default = handler; +} + +function dispose() { + Object.keys(messageHandlerRegistry).forEach(k => messageHandlerRegistry[k] = null); + Object.keys(openUrlHandler).forEach(k => openUrlHandler[k] = null); + Object.keys(sendLspNotificationHandler).forEach(k => sendLspNotificationHandler[k] = null); +} + +const MessageHandlerRegistry = { + add, + remove, + get, + getDefault, + setDefault, + openUrl, + setOpenUrlHandler, + sendLspNotification, + setSendLspNotificationHandler, + dispose +}; + +export default MessageHandlerRegistry; diff --git a/lib/reducers/index.js b/lib/reducers/index.js new file mode 100644 index 0000000..a279b59 --- /dev/null +++ b/lib/reducers/index.js @@ -0,0 +1,238 @@ +'use babel'; +'use strict'; + +import { combineReducers } from 'redux'; +import { actions } from '../actions'; + +const streamsV5Build = (state = [], action) => { + console.log('buildV5Reducer Action: ', action); + console.log('buildV5Reducer State before modification: ', state); + + switch (action.type) { + case actions.SET_BUILD_ORIGINATOR: + return { + ...state, + buildOriginator: `${action.originator}::${action.version}` + }; + case actions.PACKAGE_ACTIVATED: + return { + ...state, + packageActivated: true + }; + case actions.SET_ICP4D_URL: + return { + ...state, + icp4dUrl: action.icp4dUrl + }; + case actions.SET_USE_ICP4D_MASTER_NODE_HOST: + return { + ...state, + useIcp4dMasterNodeHost: action.useIcp4dMasterNodeHost + }; + case actions.SET_CURRENT_LOGIN_STEP: + return { + ...state, + currentLoginStep: action.currentLoginStep + }; + case actions.SET_USERNAME: + return { + ...state, + formData: { + ...state.formData, + username: action.username + } + }; + case actions.SET_PASSWORD: + return { + ...state, + formData: { + ...state.formData, + password: action.password + } + }; + case actions.SET_REMEMBER_PASSWORD: + return { + ...state, + formData: { + ...state.formData, + rememberPassword: action.rememberPassword + } + }; + case actions.SET_FORM_DATA_FIELD: + return { + ...state, + formData: { + ...state.formData, + [action.key]: action.value + } + }; + case actions.LOGIN_FORM_INITIALIZED: + return { + ...state, + formData: { + ...state.formData, + loginFormInitialized: true + } + }; + case actions.QUEUE_ACTION: + return { + ...state, + queuedAction: action.queuedAction + }; + case actions.CLEAR_QUEUED_ACTION: + return { + ...state, + queuedAction: null + }; + case actions.AUTHENTICATE_ICP4D: + return { + ...state, + username: action.username, + rememberPassword: action.rememberPassword + }; + case actions.SET_STREAMS_INSTANCES: + return { + ...state, + streamsInstances: action.streamsInstances + }; + case actions.SET_SELECTED_INSTANCE: + return { + ...state, + currentLoginStep: action.currentLoginStep, + selectedInstance: { + serviceInstanceId: action.ID, + instanceName: action.ServiceInstanceDisplayName, + serviceInstanceVersion: action.ServiceInstanceVersion, + streamsRestUrl: action.CreateArguments['connection-info'].externalRestEndpoint, + streamsBuildRestUrl: action.CreateArguments['connection-info'].externalBuildEndpoint, + streamsConsoleUrl: action.CreateArguments['connection-info'].externalConsoleEndpoint, + streamsJmxUrl: action.CreateArguments['connection-info'].externalJmxEndpoint + } + }; + case actions.SET_ICP4D_AUTH_TOKEN: + return { + ...state, + icp4dAuthToken: action.authToken, + currentLoginStep: action.currentLoginStep + }; + case actions.SET_ICP4D_AUTH_ERROR: + return { + ...state, + icp4dAuthError: action.authError, + ...(!action.authError && { formData: {} }) // RESET FORM DATA TO EMPTY + }; + case actions.SET_STREAMS_AUTH_TOKEN: + return { + ...state, + selectedInstance: { + ...state.selectedInstance, + streamsAuthToken: action.authToken + } + }; + case actions.SET_STREAMS_AUTH_ERROR: + return { + ...state, + streamsAuthError: action.authError + }; + case actions.NEW_BUILD: + return { + ...state, + builds: { + ...state.builds, + [state.selectedInstance.instanceName]: { + ...(state.builds && state.builds[state.selectedInstance.instanceName]), + newBuild: { + appRoot: action.appRoot, + toolkitRootPath: action.toolkitRootPath, + fqn: action.fqn, + makefilePath: action.makefilePath, + postBuildAction: action.postBuildAction + } + } + } + }; + case actions.GET_BUILD_STATUS_FULFILLED: + return { + ...state, + builds: { + ...state.builds, + [state.selectedInstance.instanceName]: { + ...(state.builds && state.builds[state.selectedInstance.instanceName]), + [action.buildId]: { + ...state.builds[state.selectedInstance.instanceName][action.buildId], + status: action.status, + inactivityTimeout: action.inactivityTimeout, + lastActivityTime: action.lastActivityTime, + submitCount: action.submitCount, + buildId: action.buildId + } + } + } + }; + case actions.GET_BUILD_LOG_MESSAGES_FULFILLED: + return { + ...state, + builds: { + ...state.builds, + [state.selectedInstance.instanceName]: { + ...(state.builds && state.builds[state.selectedInstance.instanceName]), + [action.buildId]: { + ...state.builds[state.selectedInstance.instanceName][action.buildId], + logMessages: action.logMessages + } + } + } + }; + case actions.BUILD_UPLOAD_SOURCE: + return { + ...state, + builds: { + ...state.builds, + [state.selectedInstance.instanceName]: { + ...(state.builds && state.builds[state.selectedInstance.instanceName]), + [action.buildId]: { + ...state.builds[state.selectedInstance.instanceName][action.buildId], + buildId: action.buildId, + appRoot: state.builds[state.selectedInstance.instanceName].newBuild.appRoot, + toolkitRootPath: state.builds[state.selectedInstance.instanceName].newBuild.toolkitRootPath, + fqn: state.builds[state.selectedInstance.instanceName].newBuild.fqn, + makefilePath: state.builds[state.selectedInstance.instanceName].newBuild.makefilePath, + postBuildAction: state.builds[state.selectedInstance.instanceName].newBuild.postBuildAction + } + } + } + }; + case actions.GET_BUILD_ARTIFACTS_FULFILLED: + return { + ...state, + builds: { + ...state.builds, + [state.selectedInstance.instanceName]: { + ...(state.builds && state.builds[state.selectedInstance.instanceName]), + [action.buildId]: { + ...state.builds[state.selectedInstance.instanceName][action.buildId], + artifacts: action.artifacts + } + } + } + }; + case actions.SET_TOOLKITS_CACHE_DIR: + return { + ...state, + toolkitsCacheDir: action.toolkitsCacheDir + }; + case actions.SET_TOOLKITS_PATH_SETTING: + return { + ...state, + toolkitsPathSetting: action.toolkitsPathSetting + }; + default: + return state; + } +}; + +const rootReducer = combineReducers({ + streamsV5Build, +}); + +export default rootReducer; diff --git a/lib/redux-store/configure-store.js b/lib/redux-store/configure-store.js new file mode 100644 index 0000000..4a0047b --- /dev/null +++ b/lib/redux-store/configure-store.js @@ -0,0 +1,38 @@ +'use babel'; +'use strict'; + +import { createStore, applyMiddleware, compose } from 'redux'; +import { createEpicMiddleware } from 'redux-observable'; +import { composeWithDevTools } from 'remote-redux-devtools'; + +import rootEpic from '../epics'; +import rootReducer from '../reducers'; + +const epicMiddleware = createEpicMiddleware(); + +let store; + +const composeEnhancers = composeWithDevTools ? composeWithDevTools({ hostname: 'localhost', port: 8000, realtime: true }) : compose; + +const addLoggingToDispatch = (s) => { + const rawDispatch = s.dispatch; + return (action) => { + console.log('store dispatch receiving action:', action); + return rawDispatch(action); + }; +}; + +export default function getStore() { + if (!store) { + store = createStore( + rootReducer, + composeEnhancers( + applyMiddleware(epicMiddleware) + ) + ); + store.dispatch = addLoggingToDispatch(store); + + epicMiddleware.run(rootEpic); + } + return store; +} diff --git a/lib/spl-build-common.js b/lib/spl-build-common.js index 69ebcd8..336f1e1 100644 --- a/lib/spl-build-common.js +++ b/lib/spl-build-common.js @@ -1,950 +1,805 @@ -// @flow - -"use babel"; -"use strict"; - -import * as path from "path"; -import * as fs from "fs"; -import * as _ from "underscore"; - -import { Observable, of, empty, forkJoin, interval } from "rxjs"; -import { switchMap, map, expand, filter, tap, debounceTime, mergeMap, takeUntil } from "rxjs/operators"; -import * as ncp from "copy-paste"; - -const request = require("request"); -request.defaults({jar: true}); - -const defaultIgnoreFiles = [ - ".git", - ".project", - ".classpath", - "toolkit.xml", - ".build*zip", - "___bundle.zip" -]; - -const defaultIgnoreDirectories = [ - "output", - "doc", - "samples", - "opt/client", - ".settings", - ".apt_generated", - ".build*", - "___bundle" -]; +'use babel'; +'use strict'; -const buildConsoleUrl = (url, instanceId) => `${url}#application/dashboard/Application%20Dashboard?instance=${instanceId}`; +import * as path from 'path'; +import * as fs from 'fs'; +import * as _ from 'lodash'; -const ibmCloudDashboardUrl = "https://cloud.ibm.com/resources"; +import { + Observable, of, empty, forkJoin, interval +} from 'rxjs'; +import { + switchMap, map, expand, filter, tap, debounceTime, mergeMap, takeUntil +} from 'rxjs/operators'; +import * as clipboardy from 'clipboardy'; -export class SplBuilder { - static BUILD_ACTION = {DOWNLOAD: 0, SUBMIT: 1}; - static SPL_MSG_REGEX = /^([\w.]+(?:\/[\w.]+)?)\:(\d+)\:(\d+)\:\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|INFO)\:.*)$/; - static SPL_NAMESPACE_REGEX = /^\s*(?:\bnamespace\b)\s+([a-z|A-Z|0-9|\.|\_]+)\s*\;/gm; - static SPL_MAIN_COMPOSITE_REGEX = /.*?(?:\bcomposite\b)(?:\s*|\/\/.*?|\/\*.*?\*\/)+([a-z|A-Z|0-9|\.|\_]+)(?:\s*|\/\/.*?|\/\*.*?\*\/)*\{/gm; - static STATUS_POLL_FREQUENCY = 5000; - _pollHandleMessage = 0; - - messageHandler = null; - lintHandler = null; - openUrlHandler = null; - serviceCredentials = null; - accessToken = null; - originatorString = null; - - constructor(messageHandler, lintHandler, openUrlHandler, originator) { - this.messageHandler = messageHandler; - this.lintHandler = lintHandler; - this.openUrlHandler = openUrlHandler; - this.originatorString = originator ? `${originator.originator}-${originator.version}:${originator.type}` : ""; - } - - dispose() { - } - - /** - * @param appRoot path to the root of the application to be built - * @param toolkitRootPath path to directory with toolkits to include in archive - * @param options .useMakefile : true = use makefile to build, false = use fqn and generate a makefile for it - * .makefilePath : path to makefile - * .fqn : fully qualified main composite name to build. ignored if useMakefile == true - * - */ - async buildSourceArchive(appRoot: string, toolkitRootPath: string, options: {useMakefile: boolean, makefilePath: string, fqn: string} = {useMakefile: false}) { - const archiver = require("archiver"); - - this.useMakefile = options.useMakefile; - if (options.makefilePath) { - this.makefilePath = options.makefilePath; - } - if (options.fqn) { - this.fqn = options.fqn; - } - - const appRootContents = fs.readdirSync(appRoot); - const makefilesFound = appRootContents.filter(entry => typeof(entry) === "string" && entry.toLowerCase() === "makefile"); - - const buildTarget = options.useMakefile ? " with makefile" : ` for ${options.fqn}`; - this.messageHandler.handleInfo(`Building application archive${buildTarget}...`); - - // temporary build archive filename is of format - // .build_[fqn]_[time].zip or .build_make_[parent_dir]_[time].zip for makefile build - // eg: .build_sample.Vwap_1547066810853.zip , .build_make_Vwap_1547066810853.zip - const outputFilePath = `${appRoot}${path.sep}.build_${options.useMakefile ? "make_"+appRoot.split(path.sep).pop() : options.fqn.replace("::",".")}_${Date.now()}.zip`; - - // delete existing build archive file before creating new one - // TODO: handle if file is open better (windows file locks) - try { - if (fs.existsSync(outputFilePath)) { - fs.unlinkSync(outputFilePath); - } - - const output = fs.createWriteStream(outputFilePath); - const archive = archiver("zip", { - zlib: { level: 9} // compression level - }); - //const self = this; - output.on("close", () => { - console.log("Application source archive built"); - this.messageHandler.handleInfo("Application archive created, submitting to build service..."); - }); - archive.on("warning", function(err) { - if (err.code === "ENOENT") { - } else { - throw err; - } - }); - archive.on("error", function(err) { - throw err; - }); - archive.pipe(output); - - let makefilePath = ""; - - const toolkitPaths = SplBuilder.getToolkits(toolkitRootPath); - let tkPathString = ""; - if (Array.isArray(toolkitPaths) && toolkitPaths.length > 0) { - const rootContents = fs.readdirSync(appRoot); - const newRoot = path.basename(appRoot); - let ignoreFiles = defaultIgnoreFiles; - - // if building for specific main composite, ignore makefile - if (!options.useMakefile) { - ignoreFiles = ignoreFiles.concat(makefilesFound); - } - const ignoreDirs = defaultIgnoreDirectories.map(entry => `${entry}`); - // Add files - rootContents - .filter(item => fs.lstatSync(`${appRoot}/${item}`).isFile()) - .filter(item => !_.some(ignoreFiles, name => { - if (name.includes("*")) { - const regex = new RegExp(name.replace(".","\.").replace("*",".*")); - return regex.test(item); - } else { - return item.includes(name); - } - })) - .forEach(item => archive.append(fs.readFileSync(`${appRoot}/${item}`), { name: `${newRoot}/${item}` })); - - // Add directories - rootContents - .filter(item => fs.lstatSync(`${appRoot}/${item}`).isDirectory()) - .filter(item => !_.some(ignoreDirs, name => { - if (name.includes("*")) { - const regex = new RegExp(name.replace(".","\.").replace("*",".*")); - return regex.test(item); - } else { - return item.includes(name); - } - })) - .forEach(item => archive.directory(`${appRoot}/${item}`, `${newRoot}/${item}`)); - - toolkitPaths.forEach(tk => archive.directory(tk.tkPath, `toolkits/${tk.tk}`)); - tkPathString = `:../toolkits`; - makefilePath = `${newRoot}/`; - - // Call the real Makefile - let newCommand = `main:\n\tmake -C ${newRoot}`; - archive.append(newCommand, { name: `Makefile` }); - - } else { - let ignoreList = defaultIgnoreFiles.concat(defaultIgnoreDirectories).map(entry => `${entry}/**`); - if (!options.useMakefile) { - ignoreList = ignoreList.concat(makefilesFound); - } - archive.glob("**/*", { - cwd: `${appRoot}/`, - ignore: ignoreList - }); - } - - // if building specific main composite, generate a makefile - if (options.fqn) { - const makeCmd = `main:\n\tsc -M ${options.fqn} -t $$STREAMS_INSTALL/toolkits${tkPathString}`; - archive.append(makeCmd, {name: `${makefilePath}/Makefile`}); - } - - const archiveStream = await archive.finalize(); - } catch (err) { - this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack, consoleErrorLog: false}); - return Promise.reject(err); - } - - return outputFilePath; - } - - build(action, streamingAnalyticsCredentials, input) { - - console.log("submitting application to build service"); - this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); - if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { - if (SplBuilder.BUILD_ACTION.DOWNLOAD === action) { - this.buildAndDownloadBundle(input); - } else if (SplBuilder.BUILD_ACTION.SUBMIT === action) { - this.buildAndSubmitJob(input); - } - } else { - const errorNotification = this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); - this.messageHandler.handleCredentialsMissing(errorNotification); - throw new Error("Error parsing VCAP_SERVICES environment variable"); - } - } - - buildAndDownloadBundle(input) { - const submitSourceAndWaitForBuild = this.submitSource(input).pipe( - switchMap(submitSourceBody => this.pollBuildStatus(submitSourceBody)), - map(buildStatusResult => ({...buildStatusResult, ...input})), - ); - - submitSourceAndWaitForBuild.pipe( - filter(a => a && a.status === "built"), - mergeMap(statusOutput => this.downloadBundlesObservable(statusOutput).pipe( - map(downloadOutput => ( [ statusOutput, downloadOutput ])) - )), - mergeMap(downloadResult => this.performBundleDownloads(downloadResult, input)), - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err, errorNotification, this.buildAndDownloadBundle.bind(this), input); - }, - complete => { - console.log("buildAndDownloadBundle observable complete"); - try { - if (input.filename && fs.existsSync(input.filename)) { - fs.unlinkSync(input.filename); - } - } catch (err) { - this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } - } - ); - } - - buildAndSubmitJob(input) { - const outputDir = `${path.dirname(input.filename)}${path.sep}output`; - const submitSourceAndWaitForBuild = this.submitSource(input).pipe( - switchMap(submitSourceBody => this.pollBuildStatus(submitSourceBody)), - map(buildStatusResult => ({...buildStatusResult, ...input})), - ); - - submitSourceAndWaitForBuild.pipe( - filter(a => a && a.status === "built"), - switchMap(artifacts => this.getConsoleUrlObservable().pipe( - map(consoleResponse => [ artifacts, consoleResponse ]), - map(consoleResult => { - const [ artifacts, consoleResponse ] = consoleResult; - if (consoleResponse.body["streams_console"] && consoleResponse.body["id"]) { - const consoleUrl = buildConsoleUrl(consoleResponse.body["streams_console"], consoleResponse.body["id"]); - - this.submitJobPrompt(consoleUrl, outputDir, this.submitAppObservable.bind(this), artifacts); - - } else { - this.messageHandler.handleError("Cannot retrieve Streaming Analytics Console URL"); - } - }) - )), - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err, errorNotification, this.buildAndSubmitJob.bind(this), input); - }, - complete => { - console.log("buildAndSubmitJob observable complete"); - try { - if (input.filename && fs.existsSync(input.filename)) { - fs.unlinkSync(input.filename); - } - } catch (err) { - this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } - } - ); - } - - submit(streamingAnalyticsCredentials, input) { - console.log("submit(); input:",arguments); - this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); - const self = this; - if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { - const outputDir = path.dirname(input.filename); - - this.getAccessTokenObservable().pipe( - map(accessTokenResponse => { - this.accessToken = accessTokenResponse.body.access_token; - return input; - }), - switchMap(submitInput => this.getConsoleUrlObservable().pipe( - map(consoleResponse => [ submitInput, consoleResponse ]), - map(consoleResult => { - const [ submitInput, consoleResponse ] = consoleResult; - if (consoleResponse.body["streams_console"] && consoleResponse.body["id"]) { - const consoleUrl = buildConsoleUrl(consoleResponse.body["streams_console"], consoleResponse.body["id"]); - - this.submitJobPrompt(consoleUrl, outputDir, this.submitSabObservable.bind(this), input); - - } else { - this.messageHandler.handleError("Cannot retrieve Streaming Analytics Console URL"); - } - }) - )), - - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err, errorNotification, this.submit.bind(this), [streamingAnalyticsCredentials, input]); - }, - complete => console.log("submit .sab observable complete"), - ); - } else { - const errorNotification = this.messageHandler.handleError("Unable to determine Streaming Analytics service credentials."); - this.messageHandler.handleCredentialsMissing(errorNotification); - throw new Error("Error parsing VCAP_SERVICES environment variable"); - } - } - - submitJobPrompt(consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput) { - console.log("submitJobPrompt(); input:",arguments); - let submissionTarget = "the application(s)"; - if (typeof(this.useMakefile) === "boolean") { - if(this.useMakefile) { - submissionTarget = "the application(s) for the Makefile"; - } else if (this.fqn) { - submissionTarget = this.fqn; - } - } else { - if (submissionObservableInput.filename) { - submissionTarget = submissionObservableInput.filename.split(path.sep).pop(); - } - } - - // Submission notification - let submissionNotification = null; - const dialogMessage = `Job submission - ${this.useMakefile ? this.makefilePath : submissionTarget}`; - const dialogDetail = `Submit ${submissionTarget} to your service with default configuration ` + - "or use the Streaming Analytics Console to customize the submission time configuration."; - - const dialogButtons = [ - { - label: "Submit", - callbackFn: () => { - console.log("submitButtonCallback"); - this.messageHandler.handleInfo("Submitting application to Streaming Analytics service..."); - submissionObservableFunc(submissionObservableInput).pipe( - mergeMap(submitResult => { - - const notificationButtons = [ - { - label: "Open Streaming Analytics Console", - callbackFn: () => this.openUrlHandler(consoleUrl) - } - ]; - // when build+submit from makefile/spl file, potentially multiple objects coming back - if (Array.isArray(submitResult)) { - submitResult.forEach(obj => { - if (obj.body) { - this.messageHandler.handleSuccess(`Job ${obj.body.name} is ${obj.body.health}`, {notificationButtons: notificationButtons}); - } - }); - } else { - if (submitResult.body) { - this.messageHandler.handleSuccess(`Job ${submitResult.body.name} is ${submitResult.body.health}`, {notificationButtons: notificationButtons}); - } - } - return of(submitResult); - }) - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - console.log("submitPrompt error caught, submissionObservableFunc:",submissionObservableFunc, "submissionObservableInput:",submissionObservableInput); - this.checkKnownErrors(err, errorNotification, this.submitJobPrompt.bind(this), [consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput]); - }, - complete => console.log("job submission observable complete"), - ); - this.messageHandler.dismissNotification(submissionNotification); - } - }, - { - label: "Submit via Streaming Analytics Console", - callbackFn: () => { - - if (submissionObservableInput.filename && submissionObservableInput.filename.toLowerCase().endsWith(".sab")) { - // sab is local already - this.openUrlHandler(consoleUrl); - - } else { - // need to download bundles first - this.messageHandler.handleInfo("Downloading application bundles for submission via Streaming Analytics Console..."); - this.downloadBundlesObservable(submissionObservableInput).pipe( - map(downloadOutput => ( [ submissionObservableInput, downloadOutput ])), - mergeMap(downloadResult => this.performBundleDownloads(downloadResult, null, outputDir)), - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err, errorNotification); - }, - complete => this.openUrlHandler(consoleUrl) - ); - } - this.messageHandler.dismissNotification(submissionNotification); - } - }, - ]; - - submissionNotification = this.messageHandler.handleInfo(dialogMessage,{detail: dialogDetail, notificationAutoDismiss: false, notificationButtons: dialogButtons}); - } - - - /** - * poll build status for a specific build - * @param input - */ - pollBuildStatus(input) { - let prevBuildOutput = []; - let buildMessage = `Building ${this.useMakefile? this.makefilePath : this.fqn}...`; - this.messageHandler.handleInfo(buildMessage); - return this.getBuildStatusObservable(input) - .pipe( - map((buildStatusResponse) => ({...input, ...buildStatusResponse.body})), - expand(buildStatusCombined => - !this.buildStatusIsComplete(buildStatusCombined, prevBuildOutput) - ? this.getBuildStatusObservable(buildStatusCombined).pipe( - debounceTime(SplBuilder.STATUS_POLL_FREQUENCY), - map(innerBuildStatusResponse => ({...buildStatusCombined, ...innerBuildStatusResponse.body})), - tap(s => { - if (this._pollHandleMessage % 3 === 0) { - const newOutput = this.getNewBuildOutput(s.output, prevBuildOutput); - this.messageHandler.handleInfo(buildMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); - prevBuildOutput = s.output; - } - this._pollHandleMessage++; - }) - ) - : empty() - ), - ); - } - - getNewBuildOutput(currOutput, prevOutput) { - return Array.isArray(currOutput) && Array.isArray(prevOutput) && currOutput.length > prevOutput.length - ? currOutput.slice(-(currOutput.length - prevOutput.length)) - : []; - } - - submitSource(input) { - return this.getAccessTokenObservable().pipe( - map(accessTokenResponse => { - this.accessToken = accessTokenResponse.body.access_token; - return input; - }), - switchMap(submitSourceInput => this.submitSourceBundleObservable(submitSourceInput) - .pipe( - map((submitSourceResponse)=> ({...submitSourceInput,id: submitSourceResponse.body.id, output_id: submitSourceResponse.body.output_id})) - )) - ); - } - - buildStatusIsComplete(input, prevBuildOutput) { - if (input.status === "failed") { - const failMessage = `Build failed - ${this.useMakefile ? this.makefilePath : this.fqn}`; - this.lintHandler.lint(input); - const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); - this.messageHandler.handleError(failMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); - return true; - } else if (input.status === "built") { - const successMessage = `Build succeeded - ${this.useMakefile ? this.makefilePath : this.fqn}`; - this.lintHandler.lint(input); - const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); - this.messageHandler.handleSuccess(successMessage, {detail: this.messageHandler.getLoggableMessage(newOutput)}); - return true; - } else { - return false; - } - } - - checkKnownErrors(err, errorNotification, retryCallbackFunction = null, retryInput = null) { - if (typeof(err) === "string") { - if (err.includes("CDISB4090E")) { - // additional notification with button to open IBM Cloud dashboard so the user can verify their - // service is started. - const n = this.messageHandler.handleError( - "Verify that the Streaming Analytics service is started and able to handle requests.", - { notificationButtons: [ - { - label: "Open IBM Cloud Dashboard", - callbackFn: ()=>{this.openUrlHandler(ibmCloudDashboardUrl)} - }, - { - label: "Start service and retry", - callbackFn: ()=> this.startServiceAndRetry(retryCallbackFunction, retryInput, [errorNotification, n]) - } - ]} - ); - } - } - } - - startServiceAndRetry(retryCallbackFunction, retryInput, notifications) { - if (Array.isArray(notifications)) { - notifications.map(a => this.messageHandler.dismissNotification(a)); - } - - const startingNotification = this.messageHandler.handleInfo("Streaming Analytics service is starting...", {notificationAutoDismiss: false}); - let startSuccessNotification = null; - let serviceState = null; - const poll = interval(8000); - - poll.pipe( - takeUntil(this.startServiceObservable().pipe( - map(a => { - if (a && a.body && a.body.state){ - serviceState = a.body.state - } - })) - ), - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err, errorNotification, retryCallbackFunction, retryInput); - }, - startServiceResult => { - this.messageHandler.dismissNotification(startingNotification); - if (serviceState === "STARTED") { - console.log("serviceRestartedSuccess",arguments); - console.log("retryCallbackFunction:",retryCallbackFunction); - console.log("retryCallbackInput:",retryInput); - this.messageHandler.handleSuccess("Streaming Analytics service started", {detail: "Service has been started. Retrying Build Service request..."}); - if (typeof(retryCallbackFunction) === "function" && retryInput) { - if (Array.isArray(retryInput)) { - retryCallbackFunction.apply(this, retryInput); - } else { - retryCallbackFunction(retryInput); - } - } - } else { - this.messageHandler.handleError("Error starting service"); - } - console.log("startService observable complete"); - }); - } - - openStreamingAnalyticsConsole(streamingAnalyticsCredentials) { - this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); - if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { - - this.getAccessTokenObservable().pipe( - mergeMap(response => { - this.accessToken = response.body.access_token; - return this.getConsoleUrlObservable(); - }), - map(response => { - if (response.body["streams_console"] && response.body["id"]) { - const consoleUrl = buildConsoleUrl(response.body["streams_console"], response.body["id"]); - this.openUrlHandler(consoleUrl); - } else { - this.messageHandler.handleError("Cannot retrieve Streaming Analytics Console URL"); - } - }) - ).subscribe( - next => {}, - err => { - let errorNotification = null; - if (err instanceof Error) { - errorNotification = this.messageHandler.handleError(err.name, {detail: err.message, stack: err.stack}); - } else { - errorNotification = this.messageHandler.handleError(err); - } - this.checkKnownErrors(err); - }, - complete => { - console.log("get Console URL observable complete"); - } - ); - } - } - - openCloudDashboard() { - this.openUrlHandler(ibmCloudDashboardUrl); - } - - getAccessTokenObservable() { - const iamTokenRequestOptions = { - method: "POST", - url: "https://iam.cloud.ibm.com/identity/token", - json: true, - headers: { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded" - }, - form: { - grant_type: "urn:ibm:params:oauth:grant-type:apikey", - apikey: this.serviceCredentials.apikey - } - }; - return SplBuilder.createObservableRequest(iamTokenRequestOptions); - } - - startServiceObservable() { - console.log("startServiceObservable entry"); - const startServiceRequestOptions = { - method: "PATCH", - url: this.serviceCredentials.v2_rest_url, - instance_id: `${this.serviceCredentials.v2_rest_url.split("/").pop()}`, - json: true, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json" - }, - body: { - "state": "STARTED" - } - }; - return SplBuilder.createObservableRequest(startServiceRequestOptions); - } - - getBuildStatusObservable(input) { - console.log("pollBuildStatusObservable input:", input); - const buildStatusRequestOptions = { - method: "GET", - url: `${this.serviceCredentials.v2_rest_url}/builds/${input.id}`, - qs: { - output_id: input.output_id - }, - json: true, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json" - }, - }; - return SplBuilder.createObservableRequest(buildStatusRequestOptions); - } - - submitSourceBundleObservable(input) { - console.log("submitSourceBundleObservable input:", input); - var buildPostRequestOptions = { - method: "POST", - url: `${this.serviceCredentials.v2_rest_url}/builds`, - json: true, - qs: { - originator: this.originatorString - }, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json" - }, - formData: { - file: { - value: fs.createReadStream(input.filename), - options: { - filename: input.filename.split(path.sep).pop(), - contentType: "application/zip" - } - } - } - }; - return SplBuilder.createObservableRequest(buildPostRequestOptions); - } - - downloadBundlesObservable(input) { - console.log("downloadBundlesObservable input:", input); - const observables = _.map(input.artifacts, artifact => { - const downloadBundleRequestOptions = { - method: "GET", - url: `${artifact.download}`, - encoding: null, - resolveWithFullResponse: true, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Accept": "application/octet-stream", - "filename": `${artifact.name}` - } - }; - console.log(downloadBundleRequestOptions); - return SplBuilder.createObservableRequest(downloadBundleRequestOptions); - }); - return forkJoin(observables); - } - - getConsoleUrlObservable() { - console.log("getConsoleUrlObservable"); - const getConsoleUrlRequestOptions = { - method: "GET", - url: `${this.serviceCredentials.v2_rest_url}`, - json: true, - encoding: null, - headers: { - "Authorization": `Bearer ${this.accessToken}` - } - }; - console.log(getConsoleUrlRequestOptions); - return SplBuilder.createObservableRequest(getConsoleUrlRequestOptions); - } - - submitSabObservable(input) { - console.log("submitSabObservable", input); - let jobConfig = "{}"; - const submitSabRequestOptions = { - method: "POST", - url: `${this.serviceCredentials.v2_rest_url}/jobs`, - json: true, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - }, - formData: { - job_options: { - value: jobConfig, - options: { - contentType: "application/json" - } - }, - bundle_file: { - value: fs.createReadStream(input.filename), - options: { - contentType: "application/octet-stream" - } - } - } - } - console.log(submitSabRequestOptions); - return SplBuilder.createObservableRequest(submitSabRequestOptions); - } - - submitAppObservable(input) { - console.log("submitAppObservable input:", input); - const observables = _.map(input.artifacts, artifact => { - let jobConfig = "{}"; - // TODO: Support for submitting job with job config overlay file - // if (fs.existsSync(jobConfigFile)) { - // jobConfig = fs.readFileSync(jobConfigFile, "utf8"); - // } - // console.log("job config for submit:",jobConfig); - - const submitAppRequestOptions = { - method: "POST", - url: `${artifact.submit_job}`, - json: true, - qs: { - artifact_id: artifact.id - }, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - }, - formData: { - job_options: { - value: jobConfig, - options: { - contentType: "application/json" - } - } - } - }; - console.log(submitAppRequestOptions); - return SplBuilder.createObservableRequest(submitAppRequestOptions); - }); - return forkJoin(observables); - } - - performBundleDownloads(downloadResult, input, outputDirOverride = undefined) { - const [ statusOutput, downloadOutput ] = downloadResult; - const outputDir = outputDirOverride ? outputDirOverride : `${path.dirname(input.filename)}${path.sep}output`; - try { - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); - } - } catch (err) { - throw new Error(`Error creating output directory\n${err}`); - } - - const observables = _.map(statusOutput.artifacts, artifact => { - const index = _.findIndex(statusOutput.artifacts, artifactObj => artifactObj.name === artifact.name); - const outputFile = `${outputDir}${path.sep}${artifact.name}`; - try { - if (fs.existsSync(outputFile)) { - fs.unlinkSync(outputFile); - } - fs.writeFileSync(outputFile, downloadOutput[index].body); - const notificationButtons = [ - { - label: "Copy output path", - callbackFn: () => ncp.copy(outputDir) - } - ]; - this.messageHandler.handleSuccess( - `Application ${artifact.name} bundle downloaded to output directory`, - { - detail: outputFile, - notificationButtons: notificationButtons - } - ); - return of(outputDir); - } catch (err) { - throw new Error(`Error downloading application .sab bundle\n${err}`); - } - }); - return forkJoin(observables); - } - - static createObservableRequest(options) { - return Observable.create((req) => { - request(options, (err, resp, body) => { - if (err) { - req.error(err); - } else if (body.errors && Array.isArray(body.errors)) { - req.error(body.errors.map(err => err.message).join("\n")); - } else { - req.next({resp, body}); - } - req.complete(); - }); - }); - } - - /** - * - */ - static getToolkits(toolkitRootDir) { - let validToolkitPaths = []; - if (toolkitRootDir && toolkitRootDir.trim() !== "") { - let toolkitRoots = []; - - if (toolkitRootDir.includes(",") || toolkitRootDir.includes(";")) { - toolkitRoots.push(...toolkitRootDir.split(/[,;]/)); - } else { - toolkitRoots.push(toolkitRootDir); - } - - toolkitRoots.forEach(toolkitRoot => { - if (fs.existsSync(toolkitRoot)) { - let toolkitRootContents = fs.readdirSync(toolkitRoot); - validToolkitPaths.push(...toolkitRootContents - .filter(item => fs.lstatSync(`${toolkitRoot}${path.sep}${item}`).isDirectory()) - .filter(dir => fs.readdirSync(`${toolkitRoot}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === "toolkit.xml").length > 0) - .map(tk => ({ tk: tk, tkPath: `${toolkitRoot}${path.sep}${tk}` })) - ); - } - }); - } - return validToolkitPaths; - } - - /** - * @param rootDirArray array of directories at the root of the IDE; - * corresponds to atom.project.getPaths() in Atom, - * or VSCode workspace.workspaceFolders - * @param filePath path to SPL file selected for build - */ - static getApplicationRoot(rootDirArray, filePath) { - if (typeof(filePath) === "string" && Array.isArray(rootDirArray)) { - let appDir = path.dirname(filePath); - const notWorkspaceFolder = dir => ( - !_.some(rootDirArray, folder => folder === dir) - ); - const noMatchingFiles = dir => !fs.existsSync(`${dir}${path.sep}info.xml`) && !fs.existsSync(`${dir}${path.sep}toolkit.xml`) && !fs.existsSync(`${dir}${path.sep}Makefile`) && !fs.existsSync(`${dir}${path.sep}makefile`); - while (notWorkspaceFolder(appDir) && noMatchingFiles(appDir)) { - appDir = path.resolve(`${appDir}${path.sep}..`); - } - return appDir; - } else { - throw new Error("Error getting application root path"); - } - } - - - /** - * read VCAP_SERVICES env variable, process the file it refers to. - * Expects VCAP JSON format, - * eg: {"streaming-analytics":[{"name":"service-1","credentials":{apikey:...,v2_rest_url:...}}]} - */ - static parseServiceCredentials(streamingAnalyticsCredentials) { - const vcapServicesPath = process.env.VCAP_SERVICES; - if (streamingAnalyticsCredentials && typeof(streamingAnalyticsCredentials) === "string") { - let serviceCreds = JSON.parse(streamingAnalyticsCredentials); - if (serviceCreds && serviceCreds.apikey && serviceCreds.v2_rest_url) { - return serviceCreds; - } - } else if (vcapServicesPath && typeof(vcapServicesPath) === "string") { - try { - if (fs.existsSync(vcapServicesPath)) { - let vcapServices = JSON.parse(fs.readFileSync(vcapServicesPath, "utf8")); - if (vcapServices.apikey && vcapServices.v2_rest_url) { - console.log("vcap:",vcapServices); - return {apikey: vcapServices.apikey, v2_rest_url: vcapServices.v2_rest_url}; - } - let streamingAnalytics = vcapServices["streaming-analytics"]; - if (streamingAnalytics && streamingAnalytics[0]) { - let credentials = streamingAnalytics[0].credentials; - if (credentials) { - return {apikey: credentials.apikey, v2_rest_url: credentials.v2_rest_url}; - } else { - console.log("Credentials not found in streaming-analytics service in VCAP"); - } - } else { - console.log("streaming-analytics service not found in VCAP"); - } - } else { - console.log("The VCAP file does not exist: " + vcapServicesPath); - } - } catch (error) { - console.log("Error processing VCAP file: " + vcapServicesPath, error); - } - } - return {}; - }; +const request = require('request'); + +request.defaults({ jar: true }); +const buildConsoleUrl = (url, instanceId) => `${url}#application/dashboard/Application%20Dashboard?instance=${instanceId}`; + +const ibmCloudDashboardUrl = 'https://cloud.ibm.com/resources'; +/* eslint-disable import/prefer-default-export */ +export class SplBuilder { + static BUILD_ACTION = { DOWNLOAD: 0, SUBMIT: 1 }; + + static SPL_MSG_REGEX = /^([\w.]+(?:\/[\w.]+)?):(\d+):(\d+):\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|INFO):.*)$/; + + static SPL_NAMESPACE_REGEX = /^\s*(?:\bnamespace\b)\s+([a-z|A-Z|0-9|.|_]+)\s*;/gm; + + static SPL_MAIN_COMPOSITE_REGEX = /.*?(?:\bcomposite\b)(?:\s*|\/\/.*?|\/\*.*?\*\/)+([a-z|A-Z|0-9|.|_]+)(?:\s*|\/\/.*?|\/\*.*?\*\/)*\{/gm; + + static STATUS_POLL_FREQUENCY = 5000; + + _pollHandleMessage = 0; + + messageHandler = null; + + lintHandler = null; + + openUrlHandler = null; + + serviceCredentials = null; + + accessToken = null; + + originatorString = null; + + constructor(messageHandler, lintHandler, openUrlHandler, originator, identifier) { + this.messageHandler = messageHandler; + this.lintHandler = lintHandler; + this.openUrlHandler = openUrlHandler; + this.originatorString = originator ? `${originator.originator}-${originator.version}:${originator.type}` : ''; + if (identifier) { + const { appRoot, fqn, makefilePath } = identifier; + if (fqn) { + this.useMakefile = false; + this.fqn = fqn; + } + if (makefilePath) { + this.useMakefile = true; + this.makefilePath = `${path.basename(appRoot)}${path.sep}${path.relative(appRoot, makefilePath)}`; + } + } + } + + dispose() { + } + + + build(action, streamingAnalyticsCredentials, input) { + console.log('submitting application to build service'); + this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); + if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { + if (SplBuilder.BUILD_ACTION.DOWNLOAD === action) { + this.buildAndDownloadBundle(input); + } else if (SplBuilder.BUILD_ACTION.SUBMIT === action) { + this.buildAndSubmitJob(input); + } + } else { + const errorNotification = this.messageHandler.handleError('Unable to determine Streaming Analytics service credentials.'); + this.messageHandler.handleCredentialsMissing(errorNotification); + throw new Error('Error parsing VCAP_SERVICES environment variable'); + } + } + + buildAndDownloadBundle(input) { + const submitSourceAndWaitForBuild = this.submitSource(input).pipe( + switchMap(submitSourceBody => this.pollBuildStatus(submitSourceBody)), + map(buildStatusResult => ({ ...buildStatusResult, ...input })), + ); + + submitSourceAndWaitForBuild.pipe( + filter(a => a && a.status === 'built'), + mergeMap(statusOutput => this.downloadBundlesObservable(statusOutput).pipe( + map(downloadOutput => ([statusOutput, downloadOutput])) + )), + mergeMap(downloadResult => this.performBundleDownloads(downloadResult, input)), + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.buildAndDownloadBundle.bind(this), input); + }, + complete => { + console.log('buildAndDownloadBundle observable complete'); + try { + if (input.filename && fs.existsSync(input.filename)) { + fs.unlinkSync(input.filename); + } + } catch (err) { + this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } + } + ); + } + + buildAndSubmitJob(input) { + const outputDir = `${path.dirname(input.filename)}${path.sep}output`; + const submitSourceAndWaitForBuild = this.submitSource(input).pipe( + switchMap(submitSourceBody => this.pollBuildStatus(submitSourceBody)), + map(buildStatusResult => ({ ...buildStatusResult, ...input })), + ); + + submitSourceAndWaitForBuild.pipe( + filter(a => a && a.status === 'built'), + switchMap(artifacts => this.getConsoleUrlObservable().pipe( + map(consoleResponse => [artifacts, consoleResponse]), + map(consoleResult => { + const [artifacts, consoleResponse] = consoleResult; + if (consoleResponse.body.streams_console && consoleResponse.body.id) { + const consoleUrl = buildConsoleUrl(consoleResponse.body.streams_console, consoleResponse.body.id); + + this.submitJobPrompt(consoleUrl, outputDir, this.submitAppObservable.bind(this), artifacts); + } else { + this.messageHandler.handleError('Cannot retrieve Streaming Analytics Console URL'); + } + }) + )), + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.buildAndSubmitJob.bind(this), input); + }, + complete => { + console.log('buildAndSubmitJob observable complete'); + try { + if (input.filename && fs.existsSync(input.filename)) { + fs.unlinkSync(input.filename); + } + } catch (err) { + this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } + } + ); + } + + submit(streamingAnalyticsCredentials, input) { + console.log('submit(); input:', arguments); + this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); + if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { + const outputDir = path.dirname(input.filename); + + this.getAccessTokenObservable().pipe( + map(accessTokenResponse => { + this.accessToken = accessTokenResponse.body.access_token; + return input; + }), + switchMap(submitInput => this.getConsoleUrlObservable().pipe( + map(consoleResponse => [submitInput, consoleResponse]), + map(consoleResult => { + const [submitInput, consoleResponse] = consoleResult; + if (consoleResponse.body.streams_console && consoleResponse.body.id) { + const consoleUrl = buildConsoleUrl(consoleResponse.body.streams_console, consoleResponse.body.id); + + this.submitJobPrompt(consoleUrl, outputDir, this.submitSabObservable.bind(this), input); + } else { + this.messageHandler.handleError('Cannot retrieve Streaming Analytics Console URL'); + } + }) + )), + + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, this.submit.bind(this), [streamingAnalyticsCredentials, input]); + }, + complete => console.log('submit .sab observable complete'), + ); + } else { + const errorNotification = this.messageHandler.handleError('Unable to determine Streaming Analytics service credentials.'); + this.messageHandler.handleCredentialsMissing(errorNotification); + throw new Error('Error parsing VCAP_SERVICES environment variable'); + } + } + + submitJobPrompt(consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput) { + console.log('submitJobPrompt(); input:', arguments); + let submissionTarget = 'the application(s)'; + if (typeof (this.useMakefile) === 'boolean') { + if (this.useMakefile) { + submissionTarget = 'the application(s) for the Makefile'; + } else if (this.fqn) { + submissionTarget = this.fqn; + } + } else if (submissionObservableInput.filename) { + submissionTarget = submissionObservableInput.filename.split(path.sep).pop(); + } + + // Submission notification + let submissionNotification = null; + const dialogMessage = `Job submission - ${this.useMakefile ? this.makefilePath : submissionTarget}`; + const dialogDetail = `Submit ${submissionTarget} to your service with default configuration ` + + 'or use the Streaming Analytics Console to customize the submission time configuration.'; + + const dialogButtons = [ + { + label: 'Submit', + callbackFn: () => { + console.log('submitButtonCallback'); + submissionObservableFunc(submissionObservableInput).pipe( + mergeMap(submitResult => { + const notificationButtons = [ + { + label: 'Open Streaming Analytics Console', + callbackFn: () => this.openUrlHandler(consoleUrl) + } + ]; + // when build+submit from makefile/spl file, potentially multiple objects coming back + if (Array.isArray(submitResult)) { + submitResult.forEach(obj => { + if (obj.body) { + this.messageHandler.handleSuccess(`Job ${obj.body.name} is ${obj.body.health}`, { notificationButtons }); + } + }); + } else if (submitResult.body) { + this.messageHandler.handleSuccess(`Job ${submitResult.body.name} is ${submitResult.body.health}`, { notificationButtons }); + } + return of(submitResult); + }) + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + console.log('submitPrompt error caught, submissionObservableFunc:', submissionObservableFunc, 'submissionObservableInput:', submissionObservableInput); + this.checkKnownErrors(err, errorNotification, this.submitJobPrompt.bind(this), [consoleUrl, outputDir, submissionObservableFunc, submissionObservableInput]); + }, + complete => console.log('job submission observable complete'), + ); + this.messageHandler.dismissNotification(submissionNotification); + } + }, + { + label: 'Submit via Streaming Analytics Console', + callbackFn: () => { + if (submissionObservableInput.filename && submissionObservableInput.filename.toLowerCase().endsWith('.sab')) { + // sab is local already + this.openUrlHandler(consoleUrl); + } else { + // need to download bundles first + this.messageHandler.handleInfo('Downloading application bundle(s) for submission via Streaming Analytics Console...'); + this.downloadBundlesObservable(submissionObservableInput).pipe( + map(downloadOutput => ([submissionObservableInput, downloadOutput])), + mergeMap(downloadResult => this.performBundleDownloads(downloadResult, null, outputDir)), + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification); + }, + complete => this.openUrlHandler(consoleUrl) + ); + } + this.messageHandler.dismissNotification(submissionNotification); + } + }, + ]; + + submissionNotification = this.messageHandler.handleInfo(dialogMessage, { detail: dialogDetail, notificationAutoDismiss: false, notificationButtons: dialogButtons }); + } + + + /** + * poll build status for a specific build + * @param input + */ + pollBuildStatus(input) { + let prevBuildOutput = []; + const buildMessage = `Building ${this.useMakefile ? this.makefilePath : this.fqn}...`; + this.messageHandler.handleInfo(buildMessage); + return this.getBuildStatusObservable(input) + .pipe( + map((buildStatusResponse) => ({ ...input, ...buildStatusResponse.body })), + expand(buildStatusCombined => (!this.buildStatusIsComplete(buildStatusCombined, prevBuildOutput) + ? this.getBuildStatusObservable(buildStatusCombined).pipe( + debounceTime(SplBuilder.STATUS_POLL_FREQUENCY), + map(innerBuildStatusResponse => ({ ...buildStatusCombined, ...innerBuildStatusResponse.body })), + tap(s => { + if (this._pollHandleMessage % 3 === 0) { + const newOutput = this.getNewBuildOutput(s.output, prevBuildOutput); + this.messageHandler.handleInfo(buildMessage, { detail: this.messageHandler.getLoggableMessage(newOutput) }); + prevBuildOutput = s.output; + } + this._pollHandleMessage += 1; + }) + ) + : empty())), + ); + } + + getNewBuildOutput(currOutput, prevOutput) { + return Array.isArray(currOutput) && Array.isArray(prevOutput) && currOutput.length > prevOutput.length + ? currOutput.slice(-(currOutput.length - prevOutput.length)) + : []; + } + + submitSource(input) { + return this.getAccessTokenObservable().pipe( + map(accessTokenResponse => { + this.accessToken = accessTokenResponse.body.access_token; + return input; + }), + switchMap(submitSourceInput => this.submitSourceBundleObservable(submitSourceInput) + .pipe( + map((submitSourceResponse) => ({ ...submitSourceInput, id: submitSourceResponse.body.id, output_id: submitSourceResponse.body.output_id })) + )) + ); + } + + buildStatusIsComplete(input, prevBuildOutput) { + if (input.status === 'failed') { + const failMessage = `Build failed - ${this.useMakefile ? this.makefilePath : this.fqn}`; + this.lintHandler.lint(input); + const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); + this.messageHandler.handleError(failMessage, { detail: this.messageHandler.getLoggableMessage(newOutput) }); + return true; + } if (input.status === 'built') { + const successMessage = `Build succeeded - ${this.useMakefile ? this.makefilePath : this.fqn}`; + this.lintHandler.lint(input); + const newOutput = this.getNewBuildOutput(input.output, prevBuildOutput); + this.messageHandler.handleSuccess(successMessage, { detail: this.messageHandler.getLoggableMessage(newOutput) }); + return true; + } + return false; + } + + checkKnownErrors(err, errorNotification, retryCallbackFunction = null, retryInput = null) { + if (typeof (err) === 'string') { + if (err.includes('CDISB4090E')) { + // additional notification with button to open IBM Cloud dashboard so the user can verify their + // service is started. + const n = this.messageHandler.handleError( + 'Verify that the Streaming Analytics service is started and able to handle requests.', + { + notificationButtons: [ + { + label: 'Open IBM Cloud Dashboard', + callbackFn: () => { this.openUrlHandler(ibmCloudDashboardUrl); } + }, + { + label: 'Start service and retry', + callbackFn: () => this.startServiceAndRetry(retryCallbackFunction, retryInput, [errorNotification, n]) + } + ] + } + ); + } + } + } + + startServiceAndRetry(retryCallbackFunction, retryInput, notifications) { + if (Array.isArray(notifications)) { + notifications.map(a => this.messageHandler.dismissNotification(a)); + } + + const startingNotification = this.messageHandler.handleInfo('Streaming Analytics service is starting...', { notificationAutoDismiss: false }); + const startSuccessNotification = null; + let serviceState = null; + const poll = interval(8000); + + poll.pipe( + takeUntil(this.startServiceObservable().pipe( + map(a => { + if (a && a.body && a.body.state) { + serviceState = a.body.state; + } + }) + )), + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err, errorNotification, retryCallbackFunction, retryInput); + }, + startServiceResult => { + this.messageHandler.dismissNotification(startingNotification); + if (serviceState === 'STARTED') { + console.log('serviceRestartedSuccess', arguments); + console.log('retryCallbackFunction:', retryCallbackFunction); + console.log('retryCallbackInput:', retryInput); + this.messageHandler.handleSuccess('Streaming Analytics service started', { detail: 'Service has been started. Retrying Build Service request...' }); + if (typeof (retryCallbackFunction) === 'function' && retryInput) { + if (Array.isArray(retryInput)) { + retryCallbackFunction.apply(this, retryInput); + } else { + retryCallbackFunction(retryInput); + } + } + } else { + this.messageHandler.handleError('Error starting service'); + } + console.log('startService observable complete'); + } + ); + } + + openStreamingAnalyticsConsole(streamingAnalyticsCredentials) { + this.serviceCredentials = SplBuilder.parseServiceCredentials(streamingAnalyticsCredentials); + if (this.serviceCredentials.apikey && this.serviceCredentials.v2_rest_url) { + this.getAccessTokenObservable().pipe( + mergeMap(response => { + this.accessToken = response.body.access_token; + return this.getConsoleUrlObservable(); + }), + map(response => { + if (response.body.streams_console && response.body.id) { + const consoleUrl = buildConsoleUrl(response.body.streams_console, response.body.id); + this.openUrlHandler(consoleUrl); + } else { + this.messageHandler.handleError('Cannot retrieve Streaming Analytics Console URL'); + } + }) + ).subscribe( + next => { }, + err => { + let errorNotification = null; + if (err instanceof Error) { + errorNotification = this.messageHandler.handleError(err.name, { detail: err.message, stack: err.stack }); + } else { + errorNotification = this.messageHandler.handleError(err); + } + this.checkKnownErrors(err); + }, + complete => { + console.log('get Console URL observable complete'); + } + ); + } + } + + openCloudDashboard() { + this.openUrlHandler(ibmCloudDashboardUrl); + } + + getAccessTokenObservable() { + const iamTokenRequestOptions = { + method: 'POST', + url: 'https://iam.cloud.ibm.com/identity/token', + json: true, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'urn:ibm:params:oauth:grant-type:apikey', + apikey: this.serviceCredentials.apikey + } + }; + return SplBuilder.createObservableRequest(iamTokenRequestOptions); + } + + startServiceObservable() { + console.log('startServiceObservable entry'); + const startServiceRequestOptions = { + method: 'PATCH', + url: this.serviceCredentials.v2_rest_url, + instance_id: `${this.serviceCredentials.v2_rest_url.split('/').pop()}`, + json: true, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json' + }, + body: { + state: 'STARTED' + } + }; + return SplBuilder.createObservableRequest(startServiceRequestOptions); + } + + getBuildStatusObservable(input) { + console.log('pollBuildStatusObservable input:', input); + const buildStatusRequestOptions = { + method: 'GET', + url: `${this.serviceCredentials.v2_rest_url}/builds/${input.id}`, + qs: { + output_id: input.output_id + }, + json: true, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json' + }, + }; + return SplBuilder.createObservableRequest(buildStatusRequestOptions); + } + + submitSourceBundleObservable(input) { + console.log('submitSourceBundleObservable input:', input); + const buildPostRequestOptions = { + method: 'POST', + url: `${this.serviceCredentials.v2_rest_url}/builds`, + json: true, + qs: { + originator: this.originatorString + }, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json' + }, + formData: { + file: { + value: fs.createReadStream(input.filename), + options: { + filename: input.filename.split(path.sep).pop(), + contentType: 'application/zip' + } + } + } + }; + return SplBuilder.createObservableRequest(buildPostRequestOptions); + } + + downloadBundlesObservable(input) { + console.log('downloadBundlesObservable input:', input); + const observables = _.map(input.artifacts, artifact => { + const downloadBundleRequestOptions = { + method: 'GET', + url: `${artifact.download}`, + encoding: null, + resolveWithFullResponse: true, + headers: { + Authorization: `Bearer ${this.accessToken}`, + Accept: 'application/octet-stream', + filename: `${artifact.name}` + } + }; + console.log(downloadBundleRequestOptions); + return SplBuilder.createObservableRequest(downloadBundleRequestOptions); + }); + return forkJoin(observables); + } + + getConsoleUrlObservable() { + console.log('getConsoleUrlObservable'); + const getConsoleUrlRequestOptions = { + method: 'GET', + url: `${this.serviceCredentials.v2_rest_url}`, + json: true, + encoding: null, + headers: { + Authorization: `Bearer ${this.accessToken}` + } + }; + console.log(getConsoleUrlRequestOptions); + return SplBuilder.createObservableRequest(getConsoleUrlRequestOptions); + } + + submitSabObservable(input) { + console.log('submitSabObservable', input); + const jobConfig = '{}'; + const submitSabRequestOptions = { + method: 'POST', + url: `${this.serviceCredentials.v2_rest_url}/jobs`, + json: true, + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + formData: { + job_options: { + value: jobConfig, + options: { + contentType: 'application/json' + } + }, + bundle_file: { + value: fs.createReadStream(input.filename), + options: { + contentType: 'application/octet-stream' + } + } + } + }; + console.log(submitSabRequestOptions); + return SplBuilder.createObservableRequest(submitSabRequestOptions); + } + + submitAppObservable(input) { + console.log('submitAppObservable input:', input); + const observables = _.map(input.artifacts, artifact => { + this.messageHandler.handleInfo(`Submitting application ${artifact.name} to the Streaming Analytics service...`); + const jobConfig = '{}'; + // TODO: Support for submitting job with job config overlay file + // if (fs.existsSync(jobConfigFile)) { + // jobConfig = fs.readFileSync(jobConfigFile, "utf8"); + // } + // console.log("job config for submit:",jobConfig); + + const submitAppRequestOptions = { + method: 'POST', + url: `${artifact.submit_job}`, + json: true, + qs: { + artifact_id: artifact.id + }, + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + formData: { + job_options: { + value: jobConfig, + options: { + contentType: 'application/json' + } + } + } + }; + console.log(submitAppRequestOptions); + return SplBuilder.createObservableRequest(submitAppRequestOptions); + }); + return forkJoin(observables); + } + + performBundleDownloads(downloadResult, input, outputDirOverride = undefined) { + const [statusOutput, downloadOutput] = downloadResult; + const outputDir = outputDirOverride || `${path.dirname(input.filename)}${path.sep}output`; + try { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + } catch (err) { + throw new Error(`Error creating output directory\n${err}`); + } + + const observables = _.map(statusOutput.artifacts, artifact => { + const index = _.findIndex(statusOutput.artifacts, artifactObj => artifactObj.name === artifact.name); + const outputFile = `${outputDir}${path.sep}${artifact.name}`; + try { + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + fs.writeFileSync(outputFile, downloadOutput[index].body); + const notificationButtons = [ + { + label: 'Copy output path', + callbackFn: () => clipboardy.writeSync(outputDir) + } + ]; + this.messageHandler.handleSuccess( + `Application ${artifact.name} bundle downloaded to output directory`, + { + detail: outputFile, + notificationButtons + } + ); + return of(outputDir); + } catch (err) { + throw new Error(`Error downloading application .sab bundle\n${err}`); + } + }); + return forkJoin(observables); + } + + static createObservableRequest(options) { + return Observable.create((req) => { + request(options, (err, resp, body) => { + if (err) { + req.error(err); + } else if (body.errors && Array.isArray(body.errors)) { + req.error(body.errors.map(err => err.message).join('\n')); + } else { + req.next({ resp, body }); + } + req.complete(); + }); + }); + } + + static getToolkits(toolkitRootDir) { + const validToolkitPaths = []; + if (toolkitRootDir && toolkitRootDir.trim() !== '') { + const toolkitRoots = []; + + if (toolkitRootDir.includes(',') || toolkitRootDir.includes(';')) { + toolkitRoots.push(...toolkitRootDir.split(/[,;]/)); + } else { + toolkitRoots.push(toolkitRootDir); + } + + toolkitRoots.forEach(toolkitRoot => { + if (fs.existsSync(toolkitRoot)) { + const toolkitRootContents = fs.readdirSync(toolkitRoot); + validToolkitPaths.push(...toolkitRootContents + .filter(item => fs.lstatSync(`${toolkitRoot}${path.sep}${item}`).isDirectory()) + .filter(dir => fs.readdirSync(`${toolkitRoot}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === 'toolkit.xml').length > 0) + .map(tk => ({ tk, tkPath: `${toolkitRoot}${path.sep}${tk}` }))); + } + }); + } + return validToolkitPaths; + } + + /** + * @param rootDirArray array of directories at the root of the IDE; + * corresponds to atom.project.getPaths() in Atom, + * or VSCode workspace.workspaceFolders + * @param filePath path to SPL file selected for build + */ + static getApplicationRoot(rootDirArray, filePath) { + if (typeof (filePath) === 'string' && Array.isArray(rootDirArray)) { + let appDir = path.dirname(filePath); + const notWorkspaceFolder = dir => ( + !_.some(rootDirArray, folder => folder === dir) + ); + const noMatchingFiles = dir => !fs.existsSync(`${dir}${path.sep}info.xml`) && !fs.existsSync(`${dir}${path.sep}toolkit.xml`) && !fs.existsSync(`${dir}${path.sep}Makefile`) && !fs.existsSync(`${dir}${path.sep}makefile`); + while (notWorkspaceFolder(appDir) && noMatchingFiles(appDir)) { + appDir = path.resolve(`${appDir}${path.sep}..`); + } + return appDir; + } + throw new Error('Error getting application root path'); + } + + + /** + * read VCAP_SERVICES env variable, process the file it refers to. + * Expects VCAP JSON format, + * eg: {"streaming-analytics":[{"name":"service-1","credentials":{apikey:...,v2_rest_url:...}}]} + */ + static parseServiceCredentials(streamingAnalyticsCredentials) { + const vcapServicesPath = process.env.VCAP_SERVICES; + if (streamingAnalyticsCredentials && typeof (streamingAnalyticsCredentials) === 'string') { + const serviceCreds = JSON.parse(streamingAnalyticsCredentials); + if (serviceCreds && serviceCreds.apikey && serviceCreds.v2_rest_url) { + return serviceCreds; + } + } else if (vcapServicesPath && typeof (vcapServicesPath) === 'string') { + try { + if (fs.existsSync(vcapServicesPath)) { + const vcapServices = JSON.parse(fs.readFileSync(vcapServicesPath, 'utf8')); + if (vcapServices.apikey && vcapServices.v2_rest_url) { + console.log('vcap:', vcapServices); + return { apikey: vcapServices.apikey, v2_rest_url: vcapServices.v2_rest_url }; + } + const streamingAnalytics = vcapServices['streaming-analytics']; + if (streamingAnalytics && streamingAnalytics[0]) { + const { credentials } = streamingAnalytics[0]; + if (credentials) { + return { apikey: credentials.apikey, v2_rest_url: credentials.v2_rest_url }; + } + console.log('Credentials not found in streaming-analytics service in VCAP'); + } else { + console.log('streaming-analytics service not found in VCAP'); + } + } else { + console.log(`The VCAP file does not exist: ${vcapServicesPath}`); + } + } catch (error) { + console.log(`Error processing VCAP file: ${vcapServicesPath}`, error); + } + } + return {}; + } } diff --git a/lib/spl-build.js b/lib/spl-build.js index 5ba312c..4d75157 100644 --- a/lib/spl-build.js +++ b/lib/spl-build.js @@ -1,346 +1,817 @@ -// @flow - -"use babel"; -"use strict"; - - -import * as fs from "fs"; -import path from "path"; - -import * as electron from "electron"; -import { CompositeDisposable } from "atom"; - -import { MessageHandler } from "./MessageHandler"; -import { LintHandler } from "./LintHandler"; -import { SplBuilder } from "./spl-build-common"; -import { MainCompositePickerView } from "./views/MainCompositePickerView"; - -import { version } from "../package.json"; - -const CONF_TOOLKITS_PATH = "ide-ibmstreams.toolkitsPath"; -const CONF_STREAMING_ANALYTICS_CREDENTIALS = "build-ibmstreams.streamingAnalyticsCredentials"; +'use babel'; +'use strict'; + + +import * as fs from 'fs'; +import path from 'path'; + +import * as electron from 'electron'; +import { CompositeDisposable } from 'atom'; + +import MessageHandler from './MessageHandler'; +import MessageHandlerRegistry from './message-handler-registry'; +import LintHandler from './LintHandler'; +import LintHandlerRegistry from './lint-handler-registry'; +import { SplBuilder } from './spl-build-common'; +import MainCompositePickerView from './views/MainCompositePickerView'; + +import KeychainUtils from './util/keychain-utils'; +import StreamsUtils from './util/streams-utils'; +import SourceArchiveUtils from './util/source-archive-utils'; +import StreamsToolkitUtils from './util/streams-toolkits-utils'; +import { + setIcp4dUrl, + queueAction, + newBuild, + submitApplicationsFromBundleFiles, + setToolkitsCacheDir, + setToolkitsPathSetting, + setUsername, + setRememberPassword, + setFormDataField, + setBuildOriginator, + setUseIcp4dMasterNodeHost +} from './actions'; +import getStore from './redux-store/configure-store'; +import StateSelector from './util/state-selectors'; + +import { version } from '../package.json'; + +import Icp4dAuthenticationView from './views/icp4dAuth/Icp4dAuthenticationView'; + +const CONF_TOOLKITS_PATH = 'ide-ibmstreams.toolkitsPath'; +const CONF_STREAMING_ANALYTICS_CREDENTIALS = 'build-ibmstreams.streamingAnalyticsCredentials'; +const CONF_ICP4D_URL = 'build-ibmstreams.icp4dUrl'; +const CONF_USE_ICP4D_MASTER_NODE_HOST = 'build-ibmstreams.useIcp4dMasterNodeHost'; +const CONF_API_VERSION = 'build-ibmstreams.buildApiVersion'; +const CONF_API_VERSION_V4 = 'v4'; +const CONF_API_VERSION_V5 = 'v5'; + +function updateIcp4dUrl(urlString) { + try { + const url = new URL(urlString); /* eslint-disable-line compat/compat */ + const prunedUrl = `${url.protocol || 'https:'}//${url.host}`; + getStore().dispatch(setIcp4dUrl(prunedUrl)); + } catch (err) { + getStore().dispatch(setIcp4dUrl(null)); + } +} export default { - config: { - streamingAnalyticsCredentials: { - type: "string", - default: "", - description: "Credentials for an IBM Streaming Analytics service." - } - }, - - subscriptions: null, - mainCompositeSelectorPanel: null, - mainCompositePickerView: null, - - linterService: null, - consoleService: null, - - lintHandler: null, - messageHandler: null, - openUrlHandler: null, - splBuilder: null, - - streamingAnalyticsCredentials: null, - appRoot: null, - toolkitRoot: null, - action: null, - - initialize(state) { - - }, - - activate(state) { - console.log("spl-build:activate"); - this.subscriptions = new CompositeDisposable(); - this.subscriptions.add( - atom.commands.add( - "atom-workspace", { - "spl-build:build-submit": () => this.buildApp(SplBuilder.BUILD_ACTION.SUBMIT), - "spl-build:build-download": () => this.buildApp(SplBuilder.BUILD_ACTION.DOWNLOAD), - "spl-build:build-make-submit": () => this.buildMake(SplBuilder.BUILD_ACTION.SUBMIT), - "spl-build:build-make-download": () => this.buildMake(SplBuilder.BUILD_ACTION.DOWNLOAD), - "spl-build:submit": () => this.submit(), - "spl-build:open-console": () => this.openConsole(), - "spl-build:open-IBM-cloud-dashboard": () => this.openCloudDashboard() - } - ) - ); - - this.openUrlHandler = url => electron.shell.openExternal(url); - - this.mainCompositePickerView = new MainCompositePickerView(this.handleBuildCallback.bind(this), this.handleCancelCallback.bind(this)); - this.mainCompositeSelectorPanel = atom.workspace.addTopPanel({ - item: this.mainCompositePickerView.getElement(), - visible: false - }); - - let toolkitsPath = atom.config.get(CONF_TOOLKITS_PATH, {}); - - var self = this; - this.subscriptions.add( + config: { + streamingAnalyticsCredentials: { + type: 'string', + default: '', + description: 'Credentials for an IBM Streaming Analytics service.' + }, + icp4dUrl: { + title: 'IBM Cloud Private for Data url', + type: 'string', + default: '', + description: 'Url for IBM Cloud Private for Data - [Refresh toolkits](atom://build-ibmstreams/toolkits/refresh)' + }, + buildApiVersion: { + title: 'Build and submit system', + type: 'string', + default: CONF_API_VERSION_V5, + description: 'Building for Streams on IBM Cloud Private for Data or Streaming analytics on public IBM Cloud', + enum: [ + { value: CONF_API_VERSION_V4, description: 'IBM Cloud Streaming Analytics service' }, + { value: CONF_API_VERSION_V5, description: 'IBM Cloud Private for Data Streams addon' } + ] + }, + useIcp4dMasterNodeHost: { + title: 'Use the IBM Cloud Private for Data host for all requests', + type: 'boolean', + default: true, + description: 'Use the host specified for the IBM Cloud Private for Data url for builds' + } + }, + + subscriptions: null, + storeSubscription: null, + mainCompositeSelectorPanel: null, + mainCompositePickerView: null, + icp4dAuthenticationPanel: null, + icp4dAuthenticationView: null, + + linterService: null, + consoleService: null, + treeView: null, + + lintHandler: null, + messageHandler: null, + openUrlHandler: null, + splBuilder: null, + + streamingAnalyticsCredentials: null, + appRoot: null, + toolkitRoot: null, + action: null, + apiVersion: null, + + initialize(state) { }, + + activate(state) { + console.log('spl-build:activate'); + + this.subscriptions = new CompositeDisposable(); + + this.registerCommands(); + this.registerContextMenu(); + + MessageHandlerRegistry.setSendLspNotificationHandler((param) => this.toolkitInitService.updateLspToolkits(param)); + + // Atom config listeners + this.subscriptions.add( + atom.config.onDidChange(CONF_ICP4D_URL, {}, (event) => { + try { + const parsedUrl = new URL(event.newValue); // eslint-disable-line compat/compat + updateIcp4dUrl(parsedUrl); + } catch (err) { /* do nothing */ } + }) + ); + this.subscriptions.add( + atom.config.onDidChange(CONF_USE_ICP4D_MASTER_NODE_HOST, {}, (event) => { + getStore().dispatch(setUseIcp4dMasterNodeHost(event.newValue)); + }) + ); + this.subscriptions.add( + atom.config.onDidChange(CONF_API_VERSION, {}, (event) => { + this.apiVersion = event.newValue; + }) + ); + + // initialize from config values + this.apiVersion = atom.config.get(CONF_API_VERSION); + if (!StateSelector.getIcp4dUrl(getStore().getState())) { + updateIcp4dUrl(atom.config.get(CONF_ICP4D_URL)); + } + if (!StateSelector.getUseIcp4dMasterNodeHost(getStore().getState())) { + getStore().dispatch(setUseIcp4dMasterNodeHost(atom.config.get(CONF_USE_ICP4D_MASTER_NODE_HOST))); + } + + this.storeSubscription = getStore().subscribe(() => { + console.log('Store subscription updated state: ', getStore().getState()); + }); + + this.openUrlHandler = url => electron.shell.openExternal(url); + MessageHandlerRegistry.setOpenUrlHandler(this.openUrlHandler); + + this.mainCompositePickerView = new MainCompositePickerView(this.handleBuildCallback.bind(this), this.handleCancelCallback.bind(this)); + this.mainCompositeSelectorPanel = atom.workspace.addTopPanel({ + item: this.mainCompositePickerView.getElement(), + visible: false + }); + + this.icp4dAuthenticationView = new Icp4dAuthenticationView(getStore(), () => { + this.icp4dAuthenticationPanel.hide(); + }); + this.icp4dAuthenticationPanel = atom.workspace.addModalPanel({ + item: this.icp4dAuthenticationView.getElement(), + visible: false + }); + + this.initializeToolkitCache(); + + if (state) { + console.log('package activation state: ', state); + if (state.username) { + getStore().dispatch(setUsername(state.username)); + } + if (state.rememberPassword) { + getStore().dispatch(setRememberPassword(state.rememberPassword)); + } + } + + getStore().dispatch(setBuildOriginator('atom', version)); + + getStore().dispatch({ type: 'PACKAGE_ACTIVATED' }); + }, + + serialize() { + const username = StateSelector.getUsername(getStore().getState()); + const rememberPassword = StateSelector.getRememberPassword(getStore().getState()); + let serializedData = {}; + serializedData = username ? { ...serializedData, username } : serializedData; + serializedData = rememberPassword ? { ...serializedData, rememberPassword } : serializedData; + return serializedData; + }, + + deactivate() { + if (this.subscriptions) { + this.subscriptions.dispose(); + } + if (this.statusBarTile) { + this.statusBarTile.destroy(); + this.statusBarTile = null; + } + + if (this.tooltipDisposable) { + this.tooltipDisposable.dispose(); + } + + if (this.vcapInputView) { + this.vcapInputView.destroy(); + } + + if (this.mainCompositeSelectorPanel) { + this.mainCompositeSelectorPanel.destroy(); + } + + if (this.mainCompositePickerView) { + this.mainCompositePickerView.destroy(); + } + + if (this.icp4dAuthenticationPanel) { + this.icp4dAuthenticationPanel.destroy(); + } + if (this.icp4dAuthenticationView) { + this.icp4dAuthenticationView.destroy(); + } + + this.storeSubscription(); + }, + + registerCommands() { + this.subscriptions.add( + atom.commands.add('atom-workspace', 'spl-build:build-submit', { + displayName: 'IBM Streams: Build application and submit a job', + description: 'Build a Streams application and submit job to the Streams instance', + didDispatch: () => this.buildApp(StreamsUtils.BUILD_ACTION.SUBMIT) + }), + atom.commands.add('atom-workspace', 'spl-build:build-download', { + displayName: 'IBM Streams: Build application and download application bundle', + description: 'Build Streams application and download the compiled application bundle', + didDispatch: () => this.buildApp(StreamsUtils.BUILD_ACTION.DOWNLOAD) + }), + atom.commands.add('atom-workspace', 'spl-build:build-make-submit', { + displayName: 'IBM Streams: Build application(s) and submit job(s)', + description: 'Build Streams application(s) and submit job(s) to the Streams instance', + didDispatch: () => this.buildMake(StreamsUtils.BUILD_ACTION.SUBMIT) + }), + atom.commands.add('atom-workspace', 'spl-build:build-make-download', { + displayName: 'IBM Streams: Build application(s) and download application bundle(s)', + description: 'Build Streams application(s) and download the compiled application bundle(s)', + didDispatch: () => this.buildMake(StreamsUtils.BUILD_ACTION.DOWNLOAD) + }), + atom.commands.add('atom-workspace', 'spl-build:submit', { + displayName: 'IBM Streams: Submit application bundle to the Streams instance', + description: 'Submit Streams application to the Streams instance', + didDispatch: () => this.submit() + }), + atom.commands.add('atom-workspace', 'spl-build:open-streams-console', { + displayName: 'IBM Streams: Open Streams Console', + description: 'Streams Console instance and application management webpage', + didDispatch: () => this.openConsole() + }), + atom.commands.add('atom-workspace', 'spl-build:open-public-cloud-dashboard', { + displayName: 'IBM Streams: Open IBM Cloud Dashboard', + description: 'IBM Cloud dashboard webpage for managing Streaming Analytics services', + didDispatch: () => this.openCloudDashboard() + }), + atom.commands.add('atom-workspace', 'spl-build:open-icp4d-dashboard', { + displayName: 'IBM Streams: Open IBM Cloud Private for Data Dashboard', + description: 'IBM Cloud Private for Data Dashboard webpage for managing the IBM Streams add-on', + didDispatch: () => this.openIcp4dDashboard() + }), + ); + }, + + registerContextMenu() { + const self = this; + this.subscriptions.add( atom.contextMenu.add({ - "atom-workspace": [ - { - type: "separator" - }, - { - label: "IBM Streams", - shouldDisplay: self.shouldShowMenu, - beforeGroupContaining: ["tree-view:open-selected-entry-up"], - submenu: [ - { - label: "Build", - command: "spl-build:build-download", - shouldDisplay: self.shouldShowMenuSpl - }, - { - label: "Build and submit job", - command: "spl-build:build-submit", - shouldDisplay: self.shouldShowMenuSpl - }, - { - label: "Build", - command: "spl-build:build-make-download", - shouldDisplay: self.shouldShowMenuMake - }, - { - label: "Build and submit job(s)", - command: "spl-build:build-make-submit", - shouldDisplay: self.shouldShowMenuMake - }, - { - label: "Submit job", - command: "spl-build:submit", - shouldDisplay: self.shouldShowMenuSubmit - } - ] - }, - { - type: "separator" - } - ] + 'atom-workspace': [ + { + type: 'separator' + }, + { + label: 'IBM Streams', + shouldDisplay: self.shouldShowMenu, + beforeGroupContaining: ['tree-view:open-selected-entry-up'], + submenu: [ + { + label: 'Build', + command: 'spl-build:build-download', + shouldDisplay: self.shouldShowMenuSpl + }, + { + label: 'Build and submit job', + command: 'spl-build:build-submit', + shouldDisplay: self.shouldShowMenuSpl + }, + { + label: 'Build', + command: 'spl-build:build-make-download', + shouldDisplay: self.shouldShowMenuMake + }, + { + label: 'Build and submit job(s)', + command: 'spl-build:build-make-submit', + shouldDisplay: self.shouldShowMenuMake + }, + { + label: 'Submit job', + command: 'spl-build:submit', + shouldDisplay: self.shouldShowMenuSubmit + }, + { + label: 'Open IBM Cloud Private for Data dashboard', + command: 'spl-build:open-icp4d-dashboard', + shouldDisplay: self.shouldShowMenuV5.bind(self) + }, + { + label: 'Open IBM Cloud dashboard', + command: 'spl-build:open-IBM-cloud-dashboard', + shouldDisplay: self.shouldShowMenuV4.bind(self) + }, + { + label: 'Open IBM Streams Console', + command: 'spl-build:open-streams-console', + shouldDisplay: self.shouldShowMenu.bind(self) + } + ] + }, + { + type: 'separator' + } + ] }) - ); - - }, - - serialize() { - - }, - - deactivate() { - if (this.subscriptions) { - this.subscriptions.dispose(); - } - if (this.statusBarTile) { - this.statusBarTile.destroy(); - this.statusBarTile = null; - } - - if (this.tooltipDisposable) { - this.tooltipDisposable.dispose(); - } - - if (this.vcapInputView) { - this.vcapInputView.destroy(); - } - - if (this.mainCompositeSelectorPanel) { - this.mainCompositeSelectorPanel.destroy(); - } - - if (this.mainCompositePickerView) { - this.mainCompositePickerView.destroy(); - } - - }, - - consumeLinter(registerIndie) { - this.linterService = registerIndie({ - name: "SPL Build" - }); - this.subscriptions.add(this.linterService); - }, - - consumeConsoleView(consumeConsoleService) { - this.consumeConsoleService = consumeConsoleService; - }, - - shouldShowMenuSpl(event) { - return event.target.innerText.toLowerCase().endsWith(".spl") ? true : false; - }, - - shouldShowMenuMake(event) { - return event.target.innerText.toLowerCase() === "makefile" ? true : false; - }, - - shouldShowMenuSubmit(event) { - return event.target.innerText.toLowerCase().endsWith(".sab") ? true : false; - }, - - shouldShowMenu(event) { - return event.target.innerText.toLowerCase() === "makefile" - || event.target.innerText.toLowerCase().endsWith(".spl") - || event.target.innerText.toLowerCase().endsWith(".sab") - ? true : false; - }, - - handleBuildCallback(e) { - const selectedComp = this.mainCompositePickerView.mainComposite; - if (selectedComp) { - this.mainCompositeSelectorPanel.hide(); - const fqn = this.namespace ? `${this.namespace}::${selectedComp}` : `${selectedComp}`; - try { - this.splBuilder.buildSourceArchive(this.appRoot, this.toolkitRootDir, {useMakefile: false, fqn: fqn}) - .then( - (filename) => - this.splBuilder.build( this.action, - this.streamingAnalyticsCredentials, - {filename: filename} - ) - ); - } finally { - this.splBuilder.dispose(); - } - } - }, - - handleCancelCallback(e) { - this.mainCompositeSelectorPanel.hide(); - }, - - buildMake(action) { - this.action = action; - - const selectedMakefilePath = atom.workspace.getActivePaneItem().selectedPath; - this.appRoot = SplBuilder.getApplicationRoot(atom.project.getPaths(), selectedMakefilePath); - this.toolkitRootDir = atom.config.get(CONF_TOOLKITS_PATH); - this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); - this.consoleService = this.consumeConsoleService({id: selectedMakefilePath, name: selectedMakefilePath}); - this.subscriptions.add(this.consoleService); - this.messageHandler = new MessageHandler(this.consoleService); - this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler, {originator: "atom", version: version, type: "make"}); - - atom.workspace.open("atom://nuclide/console"); - - try { - this.splBuilder.buildSourceArchive(this.appRoot, this.toolkitRootDir, {useMakefile: true, makefilePath: selectedMakefilePath}) - .then( - (filename) => - this.splBuilder.build( this.action, - this.streamingAnalyticsCredentials, - {filename: filename} - ) - ); - } finally { - this.splBuilder.dispose(); - } - - }, - - buildApp(action) { - this.action = action; - - const selectedFilePath = atom.workspace.getActivePaneItem().selectedPath; - let fileContents = ""; - if (selectedFilePath) { - fileContents = fs.readFileSync(selectedFilePath, "utf-8"); - } - - // Parse selected SPL file to find namespace and main composites - const namespaces = []; - while (m = SplBuilder.SPL_NAMESPACE_REGEX.exec(fileContents)) {namespaces.push(m[1])} - const mainComposites = []; - while (m = SplBuilder.SPL_MAIN_COMPOSITE_REGEX.exec(fileContents)) {mainComposites.push(m[1])} - - let fqn = ""; - if (namespaces && namespaces.length > 0) { - fqn = `${namespaces[0]}::`; - this.namespace = namespaces[0]; - } - if (mainComposites.length === 1) { - fqn = `${fqn}${mainComposites[0]}`; - } - - this.appRoot = SplBuilder.getApplicationRoot(atom.project.getPaths(), selectedFilePath); - this.toolkitRootDir = atom.config.get(CONF_TOOLKITS_PATH); - this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); - this.consoleService = this.consumeConsoleService({id: fqn, name: fqn}); - this.messageHandler = new MessageHandler(this.consoleService); - this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler, {originator: "atom", version: version, type: "spl"}); - - atom.workspace.open("atom://nuclide/console"); - - // Only prompt user to pick a main composite if more/less than one main composite are found in the SPL file. - if (mainComposites.length === 1) { - try { - this.splBuilder.buildSourceArchive(this.appRoot, this.toolkitRootDir, {useMakefile: false, fqn: fqn}) - .then( - (filename) => - this.splBuilder.build( this.action, - this.streamingAnalyticsCredentials, - {filename: filename} - ) - ); - } finally { - this.splBuilder.dispose(); - } - } else { - this.mainCompositePickerView.updatePickerContent(this.namespace, mainComposites); - this.mainCompositeSelectorPanel.show(); - // handling continued in handleBuildCallback() after user input - } - }, - - /** - * Submit a selected .sab bundle file to the instance - */ - submit() { - const selectedFilePath = atom.workspace.getActivePaneItem().selectedPath; - - if (!selectedFilePath || !selectedFilePath.toLowerCase().endsWith(".sab")) { - return; - } - - const name = path.basename(selectedFilePath).split(".sab")[0]; - - let rootDir = path.dirname(selectedFilePath); - if (path.basename(rootDir) === "output") { - rootDir = path.dirname(rootDir); - } - this.appRoot = rootDir; - - atom.workspace.open("atom://nuclide/console"); - - this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); - this.consoleService = this.consumeConsoleService({id: name, name: name}); - this.messageHandler = new MessageHandler(this.consoleService); - this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); - - this.splBuilder.submit(this.streamingAnalyticsCredentials, {filename: selectedFilePath}); - - }, - - openConsole() { - this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); - this.consoleService = this.consumeConsoleService({id: name, name: name}); - this.messageHandler = new MessageHandler(this.consoleService); - this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); - this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); - this.splBuilder.openStreamingAnalyticsConsole(this.streamingAnalyticsCredentials); - }, - - openCloudDashboard() { - this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); - this.consoleService = this.consumeConsoleService({id: name, name: name}); - this.messageHandler = new MessageHandler(this.consoleService); - this.splBuilder = new SplBuilder(this.messageHandler, null, this.openUrlHandler); - this.splBuilder.openCloudDashboard(); - } + ); + }, + + consumeLinter(registerIndie) { + this.linterService = registerIndie({ + name: 'SPL Build' + }); + this.subscriptions.add(this.linterService); + }, + + consumeTreeView(treeView) { + this.treeView = treeView; + }, + + consumeConsoleView(consumeConsoleService) { + this.consumeConsoleService = (input) => { + const newConsole = consumeConsoleService(input); + // this.subscriptions.add(newConsole); + return newConsole; + }; + if (!MessageHandlerRegistry.getDefault()) { + MessageHandlerRegistry.setDefault(new MessageHandler(this.consumeConsoleService({ id: 'IBM Streams Build', name: 'IBM Streams Build' }))); + } + }, + + consumeToolkitUpdater(consumeInitializeToolkit) { + this.toolkitInitService = consumeInitializeToolkit; + }, + + shouldShowMenuSpl(event) { + return !!event.target.innerText.toLowerCase().endsWith('.spl'); + }, + + shouldShowMenuMake(event) { + return event.target.innerText.toLowerCase() === 'makefile'; + }, + + shouldShowMenuSubmit(event) { + return !!event.target.innerText.toLowerCase().endsWith('.sab'); + }, + + shouldShowMenu(event) { + return !!(event.target.innerText.toLowerCase() === 'makefile' + || event.target.innerText.toLowerCase().endsWith('.spl') + || event.target.innerText.toLowerCase().endsWith('.sab')); + }, + + shouldShowMenuV4(event) { + this.shouldShowMenu = this.shouldShowMenu.bind(this); + return this.shouldShowMenu(event) && this.apiVersion === CONF_API_VERSION_V4; + }, + + shouldShowMenuV5(event) { + this.shouldShowMenu = this.shouldShowMenu.bind(this); + return this.shouldShowMenu(event) && this.apiVersion === CONF_API_VERSION_V5; + }, + + handleBuildCallback(e) { + const selectedComp = this.mainCompositePickerView.mainComposite; + if (selectedComp) { + this.mainCompositeSelectorPanel.hide(); + if (this.apiVersion === CONF_API_VERSION_V5) { + // todo + const fqn = this.namespace ? `${this.namespace}::${selectedComp}` : `${selectedComp}`; + // const appRoot = SourceArchiveUtils.getApplicationRoot(atom.project.getPaths(), selectedFilePath); + const toolkitRootPath = atom.config.get(CONF_TOOLKITS_PATH); + let messageHandler = MessageHandlerRegistry.get(fqn); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: fqn, name: fqn }); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(fqn, messageHandler); + } + let lintHandler = LintHandlerRegistry.get(this.appRoot); + if (!lintHandler) { + lintHandler = new LintHandler(this.linterService, this.appRoot); + LintHandlerRegistry.add(lintHandler); + } + + const newBuildAction = newBuild( + { + appRoot: this.appRoot, + toolkitRootPath, + fqn, + postBuildAction: this.action + } + ); + if (!StateSelector.hasAuthenticatedToStreamsInstance(getStore().getState())) { + getStore().dispatch(queueAction(newBuildAction)); + this.showAuthPanel(); + } else { + getStore().dispatch(newBuildAction); + } + } else { + const fqn = this.namespace ? `${this.namespace}::${selectedComp}` : `${selectedComp}`; + + let messageHandler = MessageHandlerRegistry.get(fqn); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: fqn, name: fqn }); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(fqn, messageHandler); + } + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(messageHandler, this.lintHandler, this.openUrlHandler, { originator: 'atom', version, type: 'spl' }, { appRoot: this.appRoot, fqn }); + + try { + SourceArchiveUtils.buildSourceArchive( + { + appRoot: this.appRoot, + toolkitRootPath: this.toolkitRootDir, + fqn, + messageHandler: this.messageHandler + } + ).then( + (sourceArchive) => this.splBuilder.build(this.action, + this.streamingAnalyticsCredentials, + { filename: sourceArchive.archivePath }) + ); + } finally { + this.splBuilder.dispose(); + } + } + } + }, + + handleCancelCallback(e) { + this.mainCompositeSelectorPanel.hide(); + }, + + buildMake(action) { + if (this.apiVersion === CONF_API_VERSION_V5) { + if (StateSelector.getIcp4dUrl(getStore().getState())) { + this.buildMakeV5(action); + } else { + this.handleIcp4durlNotSet(); + } + } else { + this.buildMakeV4(action); + } + }, + + buildMakeV4(action) { + this.action = action; + + const selectedMakefilePath = this.treeView.selectedPaths()[0]; + this.appRoot = SourceArchiveUtils.getApplicationRoot(atom.project.getPaths(), selectedMakefilePath); + this.toolkitRootDir = atom.config.get(CONF_TOOLKITS_PATH); + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + let messageHandler = MessageHandlerRegistry.get(selectedMakefilePath); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: selectedMakefilePath, name: selectedMakefilePath }); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(selectedMakefilePath, messageHandler); + } + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(messageHandler, this.lintHandler, this.openUrlHandler, { originator: 'atom', version, type: 'make' }, { appRoot: this.appRoot, makefilePath: selectedMakefilePath }); + + atom.workspace.open('atom://nuclide/console'); + + try { + SourceArchiveUtils.buildSourceArchive( + { + appRoot: this.appRoot, + toolkitRootPath: this.toolkitRootDir, + makefilePath: selectedMakefilePath, + messageHandler + } + ).then( + (sourceArchive) => this.splBuilder.build(this.action, + this.streamingAnalyticsCredentials, + { filename: sourceArchive.archivePath }) + ); + } finally { + this.splBuilder.dispose(); + } + }, + + buildMakeV5(action) { + const selectedMakefilePath = this.treeView.selectedPaths()[0]; + const appRoot = SourceArchiveUtils.getApplicationRoot(atom.project.getPaths(), selectedMakefilePath); + const toolkitRootPath = atom.config.get(CONF_TOOLKITS_PATH); + let messageHandler = MessageHandlerRegistry.get(selectedMakefilePath); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: selectedMakefilePath, name: selectedMakefilePath }); + this.subscriptions.add(this.consoleService); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(selectedMakefilePath, messageHandler); + } + let lintHandler = LintHandlerRegistry.get(appRoot); + if (!lintHandler) { + lintHandler = new LintHandler(this.linterService, appRoot); + LintHandlerRegistry.add(lintHandler); + } + + atom.workspace.open('atom://nuclide/console'); + const newBuildAction = newBuild( + { + appRoot, + toolkitRootPath, + makefilePath: selectedMakefilePath, + postBuildAction: action + } + ); + if (!StateSelector.hasAuthenticatedToStreamsInstance(getStore().getState())) { + getStore().dispatch(queueAction(newBuildAction)); + this.showAuthPanel(); + } else { + getStore().dispatch(newBuildAction); + } + }, + + buildApp(action) { + if (this.apiVersion === CONF_API_VERSION_V5) { + if (StateSelector.getIcp4dUrl(getStore().getState())) { + this.buildAppV5(action); + } else { + this.handleIcp4dUrlNotSet(); + } + } else { + this.buildAppV4(action); + } + }, + + buildAppV4(action) { + this.action = action; + const selectedFilePath = this.treeView.selectedPaths()[0]; + const { fqn, namespace, mainComposites } = StreamsUtils.getFqnMainComposites(selectedFilePath); + this.appRoot = SourceArchiveUtils.getApplicationRoot(atom.project.getPaths(), selectedFilePath); + this.toolkitRootDir = atom.config.get(CONF_TOOLKITS_PATH); + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + + atom.workspace.open('atom://nuclide/console'); + + // Only prompt user to pick a main composite if more/less than one main composite are found in the SPL file. + if (mainComposites.length === 1) { + let messageHandler = MessageHandlerRegistry.get(fqn); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: fqn, name: fqn }); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(fqn, messageHandler); + } + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(messageHandler, this.lintHandler, this.openUrlHandler, { originator: 'atom', version, type: 'spl' }, { appRoot: this.appRoot, fqn }); + try { + SourceArchiveUtils.buildSourceArchive( + { + appRoot: this.appRoot, + toolkitRootPath: this.toolkitRootDir, + fqn, + messageHandler + } + ).then( + (sourceArchive) => this.splBuilder.build(this.action, + this.streamingAnalyticsCredentials, + { filename: sourceArchive.archivePath }) + ); + } finally { + this.splBuilder.dispose(); + } + } else { + // this.messageHandler = messageHandler; + this.namespace = namespace; + this.mainCompositePickerView.updatePickerContent(this.namespace, mainComposites); + this.mainCompositeSelectorPanel.show(); + // handling continued in handleBuildCallback() after user input + } + }, + + buildAppV5(action) { + const selectedFilePath = this.treeView.selectedPaths()[0]; + const { fqn, namespace, mainComposites } = StreamsUtils.getFqnMainComposites(selectedFilePath); + + atom.workspace.open('atom://nuclide/console'); + const appRoot = SourceArchiveUtils.getApplicationRoot(atom.project.getPaths(), selectedFilePath); + + // Only prompt user to pick a main composite if more/less than one main composite are found in the SPL file. + if (mainComposites.length === 1) { + const toolkitRootPath = atom.config.get(CONF_TOOLKITS_PATH); + let messageHandler = MessageHandlerRegistry.get(fqn); + if (!messageHandler) { + this.consoleService = this.consumeConsoleService({ id: fqn, name: fqn }); + messageHandler = new MessageHandler(this.consoleService); + MessageHandlerRegistry.add(fqn, messageHandler); + } + let lintHandler = LintHandlerRegistry.get(appRoot); + if (!lintHandler) { + lintHandler = new LintHandler(this.linterService, appRoot); + LintHandlerRegistry.add(lintHandler); + } + + const newBuildAction = newBuild( + { + appRoot, + toolkitRootPath, + fqn, + postBuildAction: action + } + ); + if (!StateSelector.hasAuthenticatedToStreamsInstance(getStore().getState())) { + getStore().dispatch(queueAction(newBuildAction)); + this.showAuthPanel(); + } else { + getStore().dispatch(newBuildAction); + } + } else { + this.appRoot = appRoot; + this.action = action; + this.namespace = namespace; + this.mainCompositePickerView.updatePickerContent(namespace, mainComposites); + this.mainCompositeSelectorPanel.show(); + // handling continued in handleBuildCallback() after user input + } + }, + + submitV5() { + const selectedFilePaths = this.treeView.selectedPaths(); + const filteredPaths = selectedFilePaths.filter(filePath => filePath.toLowerCase().endsWith('.sab')); + const bundles = filteredPaths.map(filteredPath => ({ + bundlePath: filteredPath, + jobGroup: 'default', + jobName: filteredPath.split(path.sep).pop().split('.sab')[0], + jobConfig: null, // TODO: pass in job config file + })); + const submitAction = submitApplicationsFromBundleFiles(bundles); + if (!StateSelector.hasAuthenticatedToStreamsInstance(getStore().getState())) { + getStore().dispatch(queueAction(submitAction)); + this.icp4dAuthenticationPanel.show(); + } else { + getStore().dispatch(submitAction); + } + }, + + submitV4() { + const selectedFilePath = this.treeView.selectedPaths()[0]; + + if (!selectedFilePath || !selectedFilePath.toLowerCase().endsWith('.sab')) { + return; + } + + const name = path.basename(selectedFilePath).split('.sab')[0]; + + let rootDir = path.dirname(selectedFilePath); + if (path.basename(rootDir) === 'output') { + rootDir = path.dirname(rootDir); + } + this.appRoot = rootDir; + + atom.workspace.open('atom://nuclide/console'); + + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + this.consoleService = this.consumeConsoleService({ id: name, name }); + this.messageHandler = new MessageHandler(this.consoleService); + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); + + this.splBuilder.submit(this.streamingAnalyticsCredentials, { filename: selectedFilePath }); + }, + + /** + * Submit a selected .sab bundle file to the instance + */ + submit() { + if (this.apiVersion === CONF_API_VERSION_V5) { + if (StateSelector.getIcp4dUrl(getStore().getState())) { + this.submitV5(); + } else { + this.handleIcp4dUrlNotSet(); + } + } else { + this.submitV4(); + } + }, + + handleIcp4dUrlNotSet() { + MessageHandlerRegistry.getDefault().handleIcp4dUrlNotSet(); + }, + + showAuthPanel() { + const username = StateSelector.getFormUsername(getStore().getState()) || StateSelector.getUsername(getStore().getState()); + const rememberPassword = StateSelector.getFormRememberPassword(getStore().getState()) || StateSelector.getRememberPassword(getStore().getState()); + if (username && rememberPassword) { + KeychainUtils.getCredentials(username).then(password => { + getStore().dispatch(setFormDataField('password', password)); + }); + } + this.icp4dAuthenticationPanel.show(); + }, + + openConsole() { + if (this.apiVersion === CONF_API_VERSION_V5) { + if (StateSelector.getIcp4dUrl(getStore().getState())) { + const consoleUrlString = StateSelector.getStreamsConsoleUrl(getStore().getState()); + if (consoleUrlString) { + try { + const consoleUrl = new URL(consoleUrlString); /* eslint-disable-line compat/compat */ + MessageHandlerRegistry.openUrl(`${consoleUrl}`); + } catch (err) { /* */ } + } + } else { + this.handleIcp4dUrlNotSet(); + } + } else { + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + this.consoleService = this.consumeConsoleService({ id: name, name }); + this.messageHandler = new MessageHandler(this.consoleService); + this.lintHandler = new LintHandler(this.linterService, SplBuilder.SPL_MSG_REGEX, this.appRoot); + this.splBuilder = new SplBuilder(this.messageHandler, this.lintHandler, this.openUrlHandler); + this.splBuilder.openStreamingAnalyticsConsole(this.streamingAnalyticsCredentials); + } + }, + + openCloudDashboard() { + if (this.apiVersion === CONF_API_VERSION_V4) { + this.streamingAnalyticsCredentials = atom.config.get(CONF_STREAMING_ANALYTICS_CREDENTIALS); + this.consoleService = this.consumeConsoleService({ id: name, name }); + this.messageHandler = new MessageHandler(this.consoleService); + this.splBuilder = new SplBuilder(this.messageHandler, null, this.openUrlHandler); + this.splBuilder.openCloudDashboard(); + } + }, + + openIcp4dDashboard() { + if (this.apiVersion === CONF_API_VERSION_V5) { + const icp4dUrlString = StateSelector.getIcp4dUrl(getStore().getState()); + if (icp4dUrlString) { + try { + const icp4dUrl = new URL(icp4dUrlString); /* eslint-disable-line compat/compat */ + MessageHandlerRegistry.openUrl(`${icp4dUrl}/zen/#/homepage`); + } catch (err) { /* */ } + } else { + this.handleIcp4dUrlNotSet(); + } + } + }, + + initializeToolkitCache() { + if (atom.packages.isPackageLoaded('build-ibmstreams')) { + const toolkitsCacheDir = `${atom.packages.getLoadedPackage('build-ibmstreams').path}${path.sep}toolkitsCache`; + if (!fs.existsSync(toolkitsCacheDir)) { + fs.mkdirSync(toolkitsCacheDir); + } + getStore().dispatch(setToolkitsCacheDir(toolkitsCacheDir)); + } + }, + + initializeToolkitsDirectory() { + if (atom.packages.isPackageLoaded('ide-ibmstreams')) { + const toolkitsDirectory = atom.config.get(CONF_TOOLKITS_PATH); + if (!fs.existsSync(toolkitsDirectory)) { + fs.mkdirSync(toolkitsDirectory); + } + getStore().dispatch(setToolkitsPathSetting(toolkitsDirectory)); + } + }, + + handleStreamsBuildUri(parsedUri) { + console.log('build package uri handler: ', parsedUri); + if (parsedUri.host === 'build-ibmstreams') { + // Handle a toolkit refresh request + if (parsedUri.pathname === '/toolkits/refresh') { + console.log('trigger toolkit refresh now!'); + MessageHandlerRegistry.getDefault().handleInfo('Refreshing toolkits'); + const toolkitsDirectory = atom.config.get(CONF_TOOLKITS_PATH); + if (typeof toolkitsDirectory === 'string' && toolkitsDirectory.length > 0) { + if (!fs.existsSync(toolkitsDirectory)) { + fs.mkdirSync(toolkitsDirectory); + } + getStore().dispatch(setToolkitsPathSetting(toolkitsDirectory)); + } + const toolkitInitOptions = StreamsToolkitUtils.getLangServerOptionForInitToolkits(StateSelector.getToolkitsCacheDir(getStore().getState()), StateSelector.getToolkitsPathSetting(getStore().getState())); + console.log('toolkit refresh init options', toolkitInitOptions); + MessageHandlerRegistry.sendLspNotification(toolkitInitOptions); + } + } + }, }; diff --git a/lib/util/keychain-utils.js b/lib/util/keychain-utils.js new file mode 100644 index 0000000..3075477 --- /dev/null +++ b/lib/util/keychain-utils.js @@ -0,0 +1,48 @@ +'use babel'; +'use strict'; + +import * as keytar from 'keytar'; + +const SERVICE_ID = 'ibm-icp4d-streams'; + +const getCredentials = async (username) => { + const creds = await keytar.getPassword(SERVICE_ID, username); + return creds; +}; + +const addCredentials = async (username, password) => { + await keytar.setPassword(SERVICE_ID, username, password); +}; + +const deleteCredentials = async (username) => { + await keytar.deletePassword(SERVICE_ID, username); +}; + +const getAllCredentials = async () => { + const creds = await keytar.findCredentials(SERVICE_ID); + return creds; +}; + +const deleteAllCredentials = async () => { + const credentials = await this.getAllCredentials(); + credentials.forEach((credential: { account: string, password: string }) => { + deleteCredentials(credential.account); + }); +}; + +const credentialsExist = async () => { + const credentials = await this.getAllCredentials(); + return credentials.length > 0; +}; + + +const KeychainUtils = { + getCredentials, + addCredentials, + deleteCredentials, + getAllCredentials, + deleteAllCredentials, + credentialsExist +}; + +export default KeychainUtils; diff --git a/lib/util/rest-v5-response-selector.js b/lib/util/rest-v5-response-selector.js new file mode 100644 index 0000000..22b5596 --- /dev/null +++ b/lib/util/rest-v5-response-selector.js @@ -0,0 +1,134 @@ +'use babel'; +'use strict'; + +import * as _ from 'lodash'; +/* eslint-disable import/prefer-default-export */ + + +const getBody = (response) => { + console.log('getBody response:', response); + const body = _.get(response, 'body', {}); + if (body instanceof Buffer) { + return JSON.parse(body.toString('utf8')); + } + if (typeof body === 'string') { + try { + const bodyJson = JSON.parse(body); + return bodyJson; + } catch (err) { + // throw away syntax error + } + } + if (body.messages && Array.isArray(body.messages)) { + console.log('response error, body:', body); + console.log('response error, messages:', body.messages); + + throw new Error(body.messages.map(entry => entry.message).join('\n')); + } + return body; +}; + +const getRequestObj = (response) => { + const body = getBody(response); + return _.get(body, 'requestObj', {}); +}; + +const getBuildId = (response) => { + const body = getBody(response); + let build = _.get(body, 'build', null); + if (build) { + build = build.split('/').pop(); + } + return build; +}; + +const getStatusCode = (response) => { + return response.resp.statusCode; +}; + +const getIcp4dAuthToken = (response) => { + const body = getBody(response); + return _.get(body, 'token', ''); +}; + +const getStreamsAuthToken = (response) => { + const body = getBody(response); + return _.get(body, 'AccessToken', ''); +}; + +const getStreamsInstances = (response) => { + const requestObj = getRequestObj(response); + return _.filter(requestObj, instance => instance.ServiceInstanceType === 'streams'); +}; + +const getSelectedInstance = (response, selectedInstanceName) => { + const instances = getStreamsInstances(response); + return instances.find(instance => instance.ServiceInstanceDisplayName === selectedInstanceName); +}; + +const getBuildStatus = (response) => { + const body = getBody(response); + const { + id, + creationTime, + creationUser, + lastActivityTime, + name, + processingStartTime, + processingEndTime, + status, + submitCount + } = body; + return { + buildId: id, + creationTime, + creationUser, + lastActivityTime, + name, + processingStartTime, + processingEndTime, + status, + submitCount + }; +}; + +const getBuildArtifacts = (response) => { + const body = getBody(response); + return _.get(body, 'artifacts', []); +}; + +const getSubmitInfo = (response) => { + const body = getBody(response); + return body; +}; + +const getUploadedBundleId = (response) => { + const body = getBody(response); + return body.bundleId; +}; + +const getToolkits = (response) => { + const body = getBody(response); + return _.get(body, 'toolkits', []); +}; + +const ResponseSelector = { + getStatusCode, + + getIcp4dAuthToken, + getStreamsAuthToken, + getStreamsInstances, + getSelectedInstance, + + getBuildId, + getBuildStatus, + getBuildArtifacts, + + getUploadedBundleId, + + getSubmitInfo, + + getToolkits +}; + +export default ResponseSelector; diff --git a/lib/util/source-archive-utils.js b/lib/util/source-archive-utils.js new file mode 100644 index 0000000..02741f2 --- /dev/null +++ b/lib/util/source-archive-utils.js @@ -0,0 +1,205 @@ +'use babel'; +'use strict'; + +import * as path from 'path'; +import * as fs from 'fs'; +import * as _ from 'lodash'; + +import { from } from 'rxjs'; +import { actions } from '../actions'; +import MessageHandlerRegistry from '../message-handler-registry'; + +const archiver = require('archiver'); + +const defaultIgnoreFiles = [ + '.git', + '.project', + '.classpath', + 'toolkit.xml', + '.build*zip', + '___bundle.zip' +]; + +const defaultIgnoreDirectories = [ + 'output', + 'doc', + 'samples', + 'opt/client', + '.settings', + '.apt_generated', + '.build*', + '___bundle' +]; + +function observableBuildSourceArchive(options) { + return from(buildSourceArchive(options)); +} + +async function buildSourceArchive( + { + buildId, + appRoot, + toolkitRootPath, + fqn, + makefilePath, + bundleToolkits + } = { + bundleToolkits: false + } +) { + const useMakefile = typeof (makefilePath) === 'string'; + + let messageHandler; + let displayPath = null; + if (useMakefile) { + messageHandler = MessageHandlerRegistry.get(makefilePath); + displayPath = `${path.basename(appRoot)}${path.sep}${path.relative(appRoot, makefilePath)}`; + } else { + messageHandler = MessageHandlerRegistry.get(fqn); + } + + const appRootContents = fs.readdirSync(appRoot); + const makefilesFound = appRootContents.filter(entry => typeof (entry) === 'string' && entry.toLowerCase() === 'makefile'); + + const buildTarget = useMakefile ? ` for ${displayPath}` : ` for ${fqn}`; + messageHandler.handleInfo(`Building application archive${buildTarget}...`); + + // temporary build archive filename is of format + // .build_[fqn]_[time].zip or .build_make_[parent_dir]_[time].zip for makefile build + // eg: .build_sample.Vwap_1547066810853.zip , .build_make_Vwap_1547066810853.zip + const outputFilePath = `${appRoot}${path.sep}.build_${useMakefile ? `make_${appRoot.split(path.sep).pop()}` : fqn.replace('::', '.')}_${Date.now()}.zip`; + + // delete existing build archive file before creating new one + // TODO: handle if file is open better (windows file locks) + try { + if (fs.existsSync(outputFilePath)) { + fs.unlinkSync(outputFilePath); + } + + const output = fs.createWriteStream(outputFilePath); + const archive = archiver('zip', { + zlib: { level: 9 } // compression level + }); + output.on('close', () => { + console.log('Application source archive built'); + messageHandler.handleInfo('Application archive created, submitting to build service...'); + }); + archive.on('warning', (err) => { + if (err.code !== 'ENOENT') { + throw err; + } + }); + archive.on('error', (err) => { + throw err; + }); + archive.pipe(output); + + let makefilePath = ''; + + const toolkitPaths = getToolkits(toolkitRootPath); + let tkPathString = ''; + if (toolkitPaths) { + const rootContents = fs.readdirSync(appRoot); + const newRoot = path.basename(appRoot); + let ignoreFiles = defaultIgnoreFiles; + + // if building for specific main composite, ignore makefile + if (!useMakefile) { + ignoreFiles = ignoreFiles.concat(makefilesFound); + } + const ignoreDirs = defaultIgnoreDirectories.map(entry => `${entry}`); + // Add files + rootContents + .filter(item => fs.lstatSync(`${appRoot}/${item}`).isFile()) + .filter(item => !_.some(ignoreFiles, name => { + if (name.includes('*')) { + const regex = new RegExp(name.replace('.', '\.').replace('*', '.*')); + return regex.test(item); + } + return item.includes(name); + })) + .forEach(item => archive.append(fs.readFileSync(`${appRoot}/${item}`), { name: `${newRoot}/${item}` })); + + // Add directories + rootContents + .filter(item => fs.lstatSync(`${appRoot}/${item}`).isDirectory()) + .filter(item => !_.some(ignoreDirs, name => { + if (name.includes('*')) { + const regex = new RegExp(name.replace('.', '\.').replace('*', '.*')); + return regex.test(item); + } + return item.includes(name); + })) + .forEach(item => archive.directory(`${appRoot}/${item}`, `${newRoot}/${item}`)); + + toolkitPaths.forEach(tk => archive.directory(tk.tkPath, `toolkits/${tk.tk}`)); + tkPathString = ':../toolkits'; + makefilePath = `${newRoot}/`; + + // Call the real Makefile + const newCommand = `main:\n\tmake -C ${newRoot}`; + archive.append(newCommand, { name: 'Makefile' }); + } else { + let ignoreList = defaultIgnoreFiles.concat(defaultIgnoreDirectories).map(entry => `${entry}/**`); + if (!useMakefile) { + ignoreList = ignoreList.concat(makefilesFound); + } + archive.glob('**/*', { + cwd: `${appRoot}/`, + ignore: ignoreList + }); + } + + // if building specific main composite, generate a makefile + if (fqn) { + const makeCmd = `main:\n\tsc -M ${fqn} -t $$STREAMS_INSTALL/toolkits${tkPathString}`; + archive.append(makeCmd, { name: `${makefilePath}/Makefile` }); + } + await archive.finalize(); + // return from(archive.finalize()); + return { type: actions.SOURCE_ARCHIVE_CREATED, archivePath: outputFilePath, buildId }; + } catch (err) { + messageHandler.handleError(err.name, { detail: err.message, stack: err.stack, consoleErrorLog: false }); + return { archivePromise: Promise.reject(err), archivePath: outputFilePath, buildId }; /* eslint-disable-line compat/compat */ + } + // return { archivePath: outputFilePath, buildId }; + // return outputFilePath; +} + +function getToolkits(toolkitRootDir) { + let validToolkitPaths = null; + if (toolkitRootDir && toolkitRootDir.trim() !== '') { + if (fs.existsSync(toolkitRootDir)) { + const toolkitRootContents = fs.readdirSync(toolkitRootDir); + validToolkitPaths = toolkitRootContents + .filter(item => fs.lstatSync(`${toolkitRootDir}${path.sep}${item}`).isDirectory()) + .filter(dir => fs.readdirSync(`${toolkitRootDir}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === 'toolkit.xml').length > 0) + .map(tk => ({ tk, tkPath: `${toolkitRootDir}${path.sep}${tk}` })); + } + } + return validToolkitPaths; +} + +function getApplicationRoot(rootDirArray, filePath) { + if (typeof (filePath) === 'string' && Array.isArray(rootDirArray)) { + let appDir = path.dirname(filePath); + const notWorkspaceFolder = dir => ( + !_.some(rootDirArray, folder => folder === dir) + ); + const noMatchingFiles = dir => !fs.existsSync(`${dir}${path.sep}info.xml`) && !fs.existsSync(`${dir}${path.sep}toolkit.xml`) && !fs.existsSync(`${dir}${path.sep}Makefile`) && !fs.existsSync(`${dir}${path.sep}makefile`); + while (notWorkspaceFolder(appDir) && noMatchingFiles(appDir)) { + appDir = path.resolve(`${appDir}${path.sep}..`); + } + return appDir; + } + throw new Error('Error getting application root path'); +} + +const SourceArchiveUtils = { + buildSourceArchive, + getToolkits, + getApplicationRoot, + observableBuildSourceArchive +}; + +export default SourceArchiveUtils; diff --git a/lib/util/state-selectors.js b/lib/util/state-selectors.js new file mode 100644 index 0000000..bfb2198 --- /dev/null +++ b/lib/util/state-selectors.js @@ -0,0 +1,316 @@ +'use babel'; +'use strict'; + +import * as path from 'path'; +import { createSelector } from 'reselect'; +import { Map } from 'immutable'; + +/** + * build state selectors + */ + +const getBase = (state) => Map(state.streamsV5Build); + +const getPackageActivated = createSelector( + getBase, + (base = Map()) => base.getIn(['packageActivated']) +); + +const getLoginFormInitialized = createSelector( + getBase, + (base = Map()) => base.getIn(['formData', 'loginFormInitialized']) +); + +const getBuildOriginator = createSelector( + getBase, + (base = Map()) => base.getIn(['buildOriginator']) +); + +const getQueuedAction = createSelector( + getBase, + (base = Map()) => base.getIn(['queuedAction']) +); + +const getSelectedInstance = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance']) +); + +const getBuilds = createSelector( + getBase, + (base = Map()) => base.getIn(['builds']) +); + +const getSelectedInstanceName = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'instanceName']) +); + +const getIcp4dBearerToken = createSelector( + getBase, + (base = Map()) => base.getIn(['icp4dAuthToken']) +); + +const getStreamsBearerToken = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'streamsAuthToken']) +); + +const getCurrentLoginStep = createSelector( + getBase, + (base = Map()) => base.getIn(['currentLoginStep']) +); + +const getIcp4dAuthError = createSelector( + getBase, + (base = Map()) => base.getIn(['icp4dAuthError']) +); + +const getStreamsAuthError = createSelector( + getBase, + (base = Map()) => base.getIn(['streamsAuthError']) +); + +const getServiceInstanceId = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'serviceInstanceId']) +); + +const getStreamsInstances = createSelector( + getBase, + (base = Map()) => base.getIn(['streamsInstances']) +); + +const getUsername = createSelector( + getBase, + (base = Map()) => base.getIn(['username']) +); + +const hasAuthenticatedIcp4d = (state) => typeof getIcp4dBearerToken(state) === 'string'; +const hasAuthenticatedToStreamsInstance = (state) => typeof getStreamsBearerToken(state) === 'string'; + +const getRememberPassword = createSelector( + getBase, + (base = Map()) => base.getIn(['rememberPassword']) +); + +const getFormUsername = createSelector( + getBase, + (base = Map()) => base.getIn(['formData', 'username']) +); + +const getFormPassword = createSelector( + getBase, + (base = Map()) => base.getIn(['formData', 'password']) +); + +const getFormRememberPassword = createSelector( + getBase, + (base = Map()) => base.getIn(['formData', 'rememberPassword']) +); + +// temporary build details; before getting a build id +const getNewBuild = createSelector( + getBase, + getSelectedInstanceName, + (base = Map(), selectedInstanceName) => base.getIn(['builds', selectedInstanceName, 'newBuild']) +); + +const getBuildsForSelectedInstance = createSelector( + getBase, + getSelectedInstanceName, + (base = Map(), instanceName) => base.getIn(['builds', instanceName]) +); + +// build +const getBuild = (state, buildId) => { + const base = getBase(state); + if (base) { + const builds = getBuildsForSelectedInstance(state); + if (builds) { + return builds[buildId]; + } + } + return {}; + // const builds = getBuildsForSelectedInstance(state); + // return builds[buildId]; +}; + +const getPostBuildAction = (state, buildId) => { + const build = getBuild(state, buildId); + if (build) { + return build.postBuildAction || ''; + } + return ''; +}; + +const getBuildAppRoot = (state, buildId) => getBuild(state, buildId).appRoot; + +const getBuildStatus = (state, buildId) => getBuild(state, buildId).status; + +const getBuildLogMessages = (state, buildId) => getBuild(state, buildId).logMessages; + +const getBuildArtifacts = (state, buildId) => getBuild(state, buildId).artifacts; + +// artifact object for specific artifact id of build +const getBuildArtifact = (state, buildId, artifactId) => getBuildArtifacts(state, buildId).find(artifact => artifact.id === artifactId); + +// application root path +const getProjectPath = (state, buildId) => getBuild(state, buildId).appRoot; + +// computed fs path to use for downloading artifact +const getOutputArtifactFilePath = (state, buildId, artifactId) => { + const artifact = getBuildArtifact(state, buildId, artifactId); + const projectPath = getProjectPath(state, buildId); + return `${projectPath}/output/${artifact.name}`; +}; + +const getBuildDisplayIdentifier = (state, buildId) => { + const build = getBuild(state, buildId); + return build.makefilePath ? `${path.basename(build.appRoot)}${path.sep}${path.relative(build.appRoot, build.makefilePath)}` : build.fqn; +}; + +const getMessageHandlerIdentifier = (state, buildId) => { + const build = getBuild(state, buildId); + return build.fqn || build.makefilePath; +}; + +const getToolkitsCacheDir = createSelector( + getBase, + base => { console.log(base); return base.getIn(['toolkitsCacheDir']); } +); + +const getToolkitsPathSetting = createSelector( + getBase, + base => base.getIn(['toolkitsPathSetting']) +); + +/** + * Base configuration and authentication state selectors + */ + +const getUseIcp4dMasterNodeHost = createSelector( + getBase, + (base = Map()) => base.getIn(['useIcp4dMasterNodeHost']) +); + +const getIcp4dUrl = createSelector( + getBase, + (base = Map()) => base.getIn(['icp4dUrl']) +); + +const baseGetStreamsBuildRestUrl = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'streamsBuildRestUrl']) +); + +const baseGetStreamsRestUrl = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'streamsRestUrl']) +); + +const baseGetStreamsConsoleUrl = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'streamsConsoleUrl']) +); + +const baseGetStreamsJmxUrl = createSelector( + getBase, + (base = Map()) => base.getIn(['selectedInstance', 'streamsConsoleUrl']) +); + +const getStreamsBuildRestUrl = createSelector( + getIcp4dUrl, + getUseIcp4dMasterNodeHost, + baseGetStreamsBuildRestUrl, + (icp4dUrlString, useIcp4dMasterNodeHost, buildRestUrlString) => { + let buildRestUrlStr = useIcp4dMasterNodeHost ? convertUrl(icp4dUrlString, buildRestUrlString) : buildRestUrlString; + if (buildRestUrlStr.endsWith('/builds')) { + buildRestUrlStr = buildRestUrlStr.substring(0, buildRestUrlStr.lastIndexOf('/builds')); + } + return buildRestUrlStr; + } +); + +const getStreamsRestUrl = createSelector( + getIcp4dUrl, + getUseIcp4dMasterNodeHost, + baseGetStreamsRestUrl, + (icp4dUrlString, useIcp4dMasterNodeHost, streamsRestUrlString) => { + return useIcp4dMasterNodeHost ? convertUrl(icp4dUrlString, streamsRestUrlString) : streamsRestUrlString; + } +); + +const getStreamsConsoleUrl = createSelector( + getIcp4dUrl, + getUseIcp4dMasterNodeHost, + baseGetStreamsConsoleUrl, + (icp4dUrlString, useIcp4dMasterNodeHost, streamsConsoleUrlString) => { + return useIcp4dMasterNodeHost ? convertUrl(icp4dUrlString, streamsConsoleUrlString) : streamsConsoleUrlString; + } +); + +const getStreamsJmxUrl = createSelector( + getIcp4dUrl, + getUseIcp4dMasterNodeHost, + baseGetStreamsJmxUrl, + (icp4dUrlString, useIcp4dMasterNodeHost, streamsJmxUrlString) => { + return useIcp4dMasterNodeHost ? convertUrl(icp4dUrlString, streamsJmxUrlString) : streamsJmxUrlString; + } +); + +const convertUrl = (icp4dUrlString, endpointUrlString) => { + const icp4dUrl = new URL(icp4dUrlString); /* eslint-disable-line compat/compat */ + const streamsRestUrl = new URL(endpointUrlString); /* eslint-disable-line compat/compat */ + streamsRestUrl.hostname = icp4dUrl.hostname; + return streamsRestUrl.toString(); +}; + + +const StateSelector = { + getPackageActivated, + getBuildOriginator, + getLoginFormInitialized, + getUsername, + getRememberPassword, + getCurrentLoginStep, + getIcp4dAuthError, + getStreamsAuthError, + getQueuedAction, + + getFormUsername, + getFormPassword, + getFormRememberPassword, + + getUseIcp4dMasterNodeHost, + getIcp4dUrl, + getStreamsRestUrl, + getStreamsBuildRestUrl, + getStreamsConsoleUrl, + getStreamsJmxUrl, + getIcp4dBearerToken, + hasAuthenticatedIcp4d, + getStreamsBearerToken, + hasAuthenticatedToStreamsInstance, + getSelectedInstanceName, + getServiceInstanceId, + getStreamsInstances, + + getNewBuild, + getBuildStatus, + getBuildAppRoot, + getBuildLogMessages, + getPostBuildAction, + getBuildDisplayIdentifier, + + getBuildArtifacts, + getBuildArtifact, + getOutputArtifactFilePath, + + getToolkitsCacheDir, + getToolkitsPathSetting, + + getMessageHandlerIdentifier +}; + +export default StateSelector; diff --git a/lib/util/status-utils.js b/lib/util/status-utils.js new file mode 100644 index 0000000..0746ce1 --- /dev/null +++ b/lib/util/status-utils.js @@ -0,0 +1,159 @@ +'use babel'; +'use strict'; + +import * as path from 'path'; +import * as clipboardy from 'clipboardy'; + +import MessageHandlerRegistry from '../message-handler-registry'; +import getStore from '../redux-store/configure-store'; +import { + downloadAppBundles, + submitApplications, + openStreamingAnalyticsConsole +} from '../actions'; +import StateSelector from './state-selectors'; +import StreamsUtils from './streams-utils'; +import LintHandlerRegistry from '../lint-handler-registry'; + +function buildStatusUpdate(action, state) { + console.log('buildStatusUpdate func input,', action, state); + const { buildId } = action; + const buildStatus = StateSelector.getBuildStatus(state, buildId); + const logMessages = StateSelector.getBuildLogMessages(state, buildId); + const displayIdentifier = StateSelector.getBuildDisplayIdentifier(state, buildId); + const messageHandler = getMessageHandlerForBuildId(state, buildId); + console.log(buildStatus, logMessages, messageHandler); + if (buildStatus === 'built') { + messageHandler.handleSuccess(`Build Succeeded - ${displayIdentifier}`, { + + }); + } else if (buildStatus === 'failed') { + messageHandler.handleError(`Build Failed - ${displayIdentifier}`, { detail: logMessages, showNotification: true }); + const lintHandler = getLintHandlerForBuildId(state, buildId); + lintHandler.lint(logMessages); + } else if (buildStatus === 'building') { + messageHandler.handleInfo(`Build in progress ${displayIdentifier}...`, { detail: logMessages.join('\n'), showNotification: true }); + } +} + +function appBundleDownloaded(state, buildId, artifactName, artifactOutputPath) { + const messageHandler = getMessageHandlerForBuildId(state, buildId); + const outputDir = path.dirname(artifactOutputPath); + messageHandler.handleSuccess( + `Application ${artifactName} bundle downloaded to output directory`, + { + detail: artifactOutputPath, + notificationButtons: [ + { + label: 'Copy output path', + callbackFn: () => clipboardy.writeSync(outputDir) + } + ] + } + ); +} + +function downloadOrSubmit(state, buildId) { + console.log('downloadOrSubmit', state, buildId); + const buildStatus = StateSelector.getBuildStatus(state, buildId); + if (buildStatus === 'built') { + const artifacts = StateSelector.getBuildArtifacts(state, buildId); + const messageHandler = getMessageHandlerForBuildId(state, buildId); + const identifier = StateSelector.getMessageHandlerIdentifier(state, buildId); + const postBuildAction = StateSelector.getPostBuildAction(state, buildId); + + if (StreamsUtils.BUILD_ACTION.SUBMIT === postBuildAction) { + const submissionTarget = identifier.includes('/') ? 'the application(s) for the Makefile' : identifier; + if (Array.isArray(artifacts) && artifacts.length > 0) { + messageHandler.handleInfo(`Job Submission - ${identifier}`, { + detail: `Submit ${submissionTarget} to your service instance with default configuration or use the Streams Console to customize the submission time configuration.`, + notificationAutoDismiss: false, + notificationButtons: [ + { + label: 'Submit', + callbackFn: () => { + console.log('submitCallback'); + messageHandler.handleInfo('Submitting application'); + getStore().dispatch(submitApplications(buildId, true)); + } + }, + { + label: 'Submit via Streams Console', + callbackFn: () => { + console.log('download and open console'); + messageHandler.handleInfo('Downloading application bundle(s)'); + getStore().dispatch(downloadAppBundles(buildId)); + getStore().dispatch(openStreamingAnalyticsConsole()); + } + } + ] + }); + } + } else { + getStore().dispatch(downloadAppBundles(buildId)); + } + } +} + +function submitJobStart(state, artifactName, buildId) { + console.log('job submit start'); + const messageHandler = buildId ? getMessageHandlerForBuildId(state, buildId) : MessageHandlerRegistry.getDefault(); + if (messageHandler) { + messageHandler.handleInfo(`Submitting application ${artifactName} to the Streams Instance`); + } +} + +function jobSubmitted(state, submitInfo, buildId) { + console.log('job submitted:', state, buildId, submitInfo); + const messageHandler = buildId ? getMessageHandlerForBuildId(state, buildId) : MessageHandlerRegistry.getDefault(); + if (submitInfo.status === 'running') { + messageHandler.handleSuccess( + `Job ${submitInfo.name} has been successfully submitted to the ${StateSelector.getSelectedInstanceName(state)} instance`, + { + detail: 'To monitor or manage the job, use the IBM Cloud Private for Data Manage Jobs webpage or the Streams Console.', + notificationAutoDismiss: false, + notificationButtons: [ + { + label: 'Open ICP4D Console', + callbackFn: () => { + const icp4dUrlStr = StateSelector.getIcp4dUrl(state); + const icp4dUrl = new URL(icp4dUrlStr); /* eslint-disable-line compat/compat */ + const icp4dUrlBase = `${icp4dUrl.protocol}//${icp4dUrl.host}`; + const jobDetailsUrl = `${icp4dUrlBase}/streams/webpage/#/streamsJobDetails/streams-${StateSelector.getServiceInstanceId(state)}-${submitInfo.id}`; + MessageHandlerRegistry.openUrl(jobDetailsUrl); + } + }, + { + label: 'Open Streams Console', + callbackFn: () => { + const consoleUrl = StateSelector.getStreamsConsoleUrl(state); + const jobName = submitInfo.name; + MessageHandlerRegistry.openUrl(`${consoleUrl}#application/dashboard/Application%20Dashboard?job=${jobName}`); + } + } + ] + } + ); + } +} + +function getMessageHandlerForBuildId(state, buildId) { + const identifier = StateSelector.getMessageHandlerIdentifier(state, buildId); + return MessageHandlerRegistry.get(identifier); +} + +function getLintHandlerForBuildId(state, buildId) { + const appRoot = StateSelector.getBuildAppRoot(state, buildId); + return LintHandlerRegistry.get(appRoot); +} + +const StatusUtils = { + buildStatusUpdate, + downloadOrSubmit, + appBundleDownloaded, + submitJobStart, + jobSubmitted, + getMessageHandlerForBuildId +}; + +export default StatusUtils; diff --git a/lib/util/streams-rest-v5.js b/lib/util/streams-rest-v5.js new file mode 100644 index 0000000..32f216e --- /dev/null +++ b/lib/util/streams-rest-v5.js @@ -0,0 +1,443 @@ +'use babel'; +'use strict'; + +import * as path from 'path'; +import * as fs from 'fs'; + +import { Observable } from 'rxjs'; + +import StateSelector from './state-selectors'; + +const request = require('request'); + +const baseRequestOptions = { + method: 'GET', + json: true, + gzip: true, + timeout: 60000, + agentOptions: { + rejectUnauthorized: false + }, + ecdhCurve: 'auto', + strictSSL: false, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, +}; + +const baseRequest = request.defaults(baseRequestOptions); + +/** + * StreamsRestUtil.build + */ + +function getAll(state) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds`, + auth: getStreamsAuth(state) + }; + return observableRequest(baseRequest, options); +} + +function getStatus(state, buildId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}`, + auth: getStreamsAuth(state) + }; + return observableRequest(baseRequest, options); +} + +function create( + state, + { + inactivityTimeout, + incremental, + name, + type + } = { + inactivityTimeout: 15, + incremental: true, + name: 'myBuild', + type: 'application' + } +) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds`, + auth: getStreamsAuth(state), + body: { + inactivityTimeout, + incremental, + name, + originator: StateSelector.getBuildOriginator(state) || 'unknown', + type + } + }; + return observableRequest(baseRequest, options); +} + +function deleteBuild(state, buildId) { + const options = { + method: 'DELETE', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}`, + auth: getStreamsAuth(state), + headers: { + Accept: '*/*' + } + }; + return observableRequest(baseRequest, options); +} + +function uploadSource(state, buildId, sourceZipPath) { + const options = { + method: 'PUT', + json: false, + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}`, + auth: getStreamsAuth(state), + headers: { + 'Content-Type': 'application/zip' + }, + encoding: null, + body: fs.createReadStream(sourceZipPath) + }; + return observableRequest(baseRequest, options); +} + +function updateSource(state, buildId, sourceZipPath) { + const options = { + method: 'PATCH', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}`, + auth: getStreamsAuth(state), + headers: { + 'Content-Type': 'application/zip' + }, + formData: { + file: { + value: fs.createReadStream(sourceZipPath), + options: { + filename: sourceZipPath.split(path.sep).pop(), + contentType: 'application/zip' + } + } + } + }; + return observableRequest(baseRequest, options); +} + +function getLogMessages(state, buildId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/logmessages`, + auth: getStreamsAuth(state), + json: false, + headers: { + Accept: 'text/plain' + } + }; + return observableRequest(baseRequest, options); +} + +function start(state, buildId, { buildConfigOverrides = {} } = {}) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/actions`, + auth: getStreamsAuth(state), + body: { + type: 'submit', + buildConfigOverrides + } + }; + return observableRequest(baseRequest, options); +} + +function cancel(state, buildId, { buildConfigOverrides = {} } = {}) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/actions`, + auth: getStreamsAuth(state), + body: { + type: 'cancel', + buildConfigOverrides + } + }; + return observableRequest(baseRequest, options); +} + +function getSnapshots(state) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/snapshot`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +/** + * StreamsRestUtil.artifact + */ + +function getArtifacts(state, buildId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/artifacts`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function getArtifact(state, buildId, artifactId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/artifacts/${artifactId}`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function getAdl(state, buildId, artifactId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/artifacts/${artifactId}/adl`, + auth: getStreamsAuth(state), + headers: { + Accept: 'text/xml' + } + }; + return observableRequest(baseRequest, options); +} + +function downloadApplicationBundle(state, buildId, artifactId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/builds/${buildId}/artifacts/${artifactId}/applicationbundle`, + auth: getStreamsAuth(state), + encoding: null, + headers: { + Accept: 'application/x-jar', + } + }; + return observableRequest(baseRequest, options); +} + +function uploadApplicationBundleToInstance(state, applicationBundlePath) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsRestUrl(state)}/applicationbundles`, + auth: getStreamsAuth(state), + headers: { + 'Content-Type': 'application/x-jar', + Accept: 'application/json' + }, + json: false, + body: fs.createReadStream(applicationBundlePath) + }; + return observableRequest(baseRequest, options); +} + +function submitJob( + state, + applicationBundleIdOrUrl, + { + applicationCredentials, + jobConfig, + jobGroup, + jobName, + preview, + submitParameters + } = { + preview: false, + jobGroup: 'default', + jobName: 'myJob', + submitParameters: [], + jobConfig: {}, + applicationCredentials: {} + } +) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsRestUrl(state)}/jobs`, + auth: getStreamsAuth(state), + body: { + application: applicationBundleIdOrUrl, + preview, + jobGroup, + jobName, + submitParameters, + jobConfigurationOverlay: jobConfig, + applicationCredentials: { + bearerToken: StateSelector.getStreamsBearerToken(state) + } + } + }; + return observableRequest(baseRequest, options); +} + +/** + * StreamsRestUtil.toolkit + */ + +function getToolkits(state) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/toolkits`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function getToolkit(state, toolkitId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/toolkits/${toolkitId}`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function addToolkit(state, toolkitZipPath) { + const options = { + method: 'POST', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/toolkits`, + auth: getStreamsAuth(state), + headers: { + 'Content-Type': 'application/zip' + }, + formData: { + file: { + value: fs.createReadStream(toolkitZipPath), + options: { + filename: toolkitZipPath.split(path.sep).pop(), + contentType: 'application/x-jar' + } + } + } + }; + return observableRequest(baseRequest, options); +} + +function deleteToolkit(state, toolkitId) { + const options = { + method: 'DELETE', + url: `${StateSelector.getStreamsBuildRestUrl(state)}/toolkits/${toolkitId}`, + auth: getStreamsAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function getToolkitIndex(state, toolkitId) { + const options = { + url: `${StateSelector.getStreamsBuildRestUrl(state)}/toolkits/${toolkitId}/index`, + auth: getStreamsAuth(state), + headers: { + Accept: 'text/xml' + } + }; + return observableRequest(baseRequest, options); +} + +/** + * Helper functions + */ + +function getIcp4dToken(state, username, password) { + const options = { + method: 'POST', + url: `${StateSelector.getIcp4dUrl(state)}/icp4d-api/v1/authorize`, + body: { + username, + password + }, + ecdhCurve: 'auto' + }; + return observableRequest(baseRequest, options); +} + +function getServiceInstances(state) { + const options = { + url: `${StateSelector.getIcp4dUrl(state)}/zen-data/v2/serviceInstance`, + auth: getIcp4dAuth(state), + }; + return observableRequest(baseRequest, options); +} + +function getStreamsAuthToken(state, instanceName) { + const options = { + method: 'POST', + auth: getIcp4dAuth(state), + url: `${StateSelector.getIcp4dUrl(state)}/zen-data/v2/serviceInstance/token`, + body: { + serviceInstanceDisplayname: instanceName + } + }; + return observableRequest(baseRequest, options); +} + +function observableRequest(requestInst, options) { + console.log('request options: ', options); + return Observable.create((req) => { + requestInst(options, (err, resp, body) => { + if (err) { + req.error(err); + } else if (body && body.errors && Array.isArray(body.errors)) { + req.error(body.errors.map(err1 => err1.message).join('\n')); + } else if (resp.statusCode < 200 && resp.statusCode >= 300) { + req.error(resp.statusMessage); + } else { + req.next({ resp, body }); + } + req.complete(); + }); + }); +} + +function getStreamsAuth(state) { + const token = StateSelector.getStreamsBearerToken(state); + return token ? { bearer: token } : { username: 'admin', password: 'password' }; +} + +function getIcp4dAuth(state) { + const token = StateSelector.getIcp4dBearerToken(state); + return token ? { bearer: token } : { username: 'admin', password: 'password' }; +} + +/** + * Exports + */ + +const build = { + getAll, + getStatus, + create, + deleteBuild, + uploadSource, + updateSource, + getLogMessages, + start, + cancel, + getSnapshots +}; + +const artifact = { + getArtifacts, + getArtifact, + getAdl, + downloadApplicationBundle, + uploadApplicationBundleToInstance, + submitJob +}; + +const toolkit = { + getToolkits, + getToolkit, + addToolkit, + deleteToolkit, + getToolkitIndex +}; + +const icp4d = { + getServiceInstances, + getIcp4dToken, + getStreamsAuthToken +}; + +const StreamsRestUtil = { + build, + artifact, + toolkit, + icp4d +}; + +export default StreamsRestUtil; diff --git a/lib/util/streams-toolkits-utils.js b/lib/util/streams-toolkits-utils.js new file mode 100644 index 0000000..2965efe --- /dev/null +++ b/lib/util/streams-toolkits-utils.js @@ -0,0 +1,182 @@ +'use babel'; +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as _ from 'lodash'; +import * as xmldoc from 'xmldoc'; +import StateSelector from './state-selectors'; + +function refreshLspToolkits(state, sendNotification) { + const clearParam = getLangServerParamForClearToolkits(); + sendNotification(clearParam); + + const addParam = getLangServerParamForAddToolkits([ + ...getCachedToolkitIndexPaths(StateSelector.getToolkitsCacheDir(state)), + ...getLocalToolkitIndexPaths(StateSelector.getToolkitsPathSetting(state)) + ]); + sendNotification(addParam); +} + +function getLangServerOptionForInitToolkits(toolkitsCacheDir, toolkitsPathSetting) { + return { + toolkits: { + action: 'INIT', + indexList: [ + ...getCachedToolkitIndexPaths(toolkitsCacheDir), + ...getLocalToolkitIndexPaths(toolkitsPathSetting) + ] + } + }; +} + +function getLangServerParamForAddToolkits(toolkitIndexPaths) { + return { + settings: { + toolkits: { + action: 'ADD', + indexList: toolkitIndexPaths + } + } + }; +} + +function getLangServerParamForRemoveToolkits(toolkitNames) { + return { + settings: { + toolkits: { + action: 'REMOVE', + names: toolkitNames + } + } + }; +} + +function getLangServerParamForClearToolkits() { + return { + settings: { + toolkits: { + action: 'CLEAR' + } + } + }; +} + +function getCachedToolkitIndexPaths(toolkitsCacheDir) { + try { + const filenames = fs.readdirSync(toolkitsCacheDir).filter(entry => typeof entry === 'string' && path.extname(entry) === '.xml'); + return filenames.map(filename => `${toolkitsCacheDir}${path.sep}${filename}`); + } catch (err) { + throw new Error(`Error getting cached toolkit index paths in: ${toolkitsCacheDir}\n${err}`); + } +} + +function getLocalToolkitIndexPaths(toolkitPaths) { + try { + const validToolkitIndexPaths = []; + if (toolkitPaths && toolkitPaths !== '') { + const toolkitRoots = []; + + if (toolkitPaths.includes(',') || toolkitPaths.includes(';')) { + toolkitRoots.push(...toolkitPaths.split(/[,;]/)); + } else { + toolkitRoots.push(toolkitPaths); + } + + toolkitRoots.forEach(toolkitRoot => { + if (fs.existsSync(toolkitRoot)) { + const toolkitRootContents = fs.readdirSync(toolkitRoot); + validToolkitIndexPaths.push(...toolkitRootContents + .filter(item => fs.lstatSync(`${toolkitRoot}${path.sep}${item}`).isDirectory()) + .filter(dir => fs.readdirSync(`${toolkitRoot}${path.sep}${dir}`).filter(tkDirItem => tkDirItem === 'toolkit.xml').length > 0) + .map(toolkit => `${toolkitRoot}${path.sep}${toolkit}${path.sep}toolkit.xml`)); + } + }); + } + return validToolkitIndexPaths; + } catch (err) { + throw new Error(`Error getting local toolkit index paths for: ${toolkitPaths}\n${err}`); + } +} + +function getChangedLocalToolkits(oldValue, newValue) { + const oldIndexPaths = getLocalToolkitIndexPaths(oldValue); + const newIndexPaths = getLocalToolkitIndexPaths(newValue); + const addedToolkitPaths = _.difference(newIndexPaths, oldIndexPaths); + const removedToolkitPaths = _.difference(oldIndexPaths, newIndexPaths); + const removedToolkitNames = []; + removedToolkitPaths.forEach(tkPath => { + try { + const xml = fs.readFileSync(tkPath, 'utf8'); + const document = new xmldoc.XmlDocument(xml); + const toolkitName = document.childNamed('toolkit').attr.name; + removedToolkitNames.push(toolkitName); + } catch (err) { + throw new Error(`Error reading local toolkit index contents for: ${tkPath}\n${err}`); + } + }); + + return { addedToolkitPaths, removedToolkitNames }; +} + +function getToolkitsToCache(state, buildServiceToolkits) { + const cacheDir = StateSelector.getToolkitsCacheDir(state); + if (!cacheDir) { + throw new Error('Toolkit cache directory does not exist'); + } + + const toolkitsToCache = []; + try { + const cachedToolkits = fs.readdirSync(cacheDir).filter(entry => typeof entry === 'string' && path.extname(entry) === '.xml'); + + buildServiceToolkits.forEach(toolkitObj => { + const { name, version } = toolkitObj; + let existingToolkit = cachedToolkits.filter(filename => filename.startsWith(name)); + if (existingToolkit && existingToolkit.length) { + existingToolkit = existingToolkit[0]; + const existingToolkitVersion = existingToolkit.replace(name, '').match(/-([0-9.]+).xml/)[1]; + if (version > existingToolkitVersion) { + // Replace the older version with the newer version + fs.unlinkSync(`${cacheDir}${path.sep}${existingToolkit}`); + toolkitsToCache.push(toolkitObj); + } + } else { + // Cache the new toolkit index + toolkitsToCache.push(toolkitObj); + } + }); + } catch (err) { + throw new Error(`Error getting toolkits to cache:\n${err}`); + } + + return toolkitsToCache; +} + +function cacheToolkitIndex(state, toolkit, index) { + const { name, version } = toolkit; + const cacheDir = StateSelector.getToolkitsCacheDir(state); + if (!cacheDir) { + throw new Error('Toolkit cache directory does not exist'); + } + console.log('in cacheToolkitIndex:', state, toolkit, cacheDir); + try { + fs.writeFileSync(`${cacheDir}${path.sep}${name}-${version}.xml`, index); + } catch (err) { + throw new Error(`Error caching toolkit index for: ${name}\n${err}`); + } +} + +const StreamsToolkitsUtils = { + refreshLspToolkits, + getLangServerOptionForInitToolkits, + getLangServerParamForAddToolkits, + getLangServerParamForRemoveToolkits, + getLangServerParamForClearToolkits, + getCachedToolkitIndexPaths, + getLocalToolkitIndexPaths, + getChangedLocalToolkits, + cacheToolkitIndex, + getToolkitsToCache +}; + +export default StreamsToolkitsUtils; diff --git a/lib/util/streams-utils.js b/lib/util/streams-utils.js new file mode 100644 index 0000000..c87a240 --- /dev/null +++ b/lib/util/streams-utils.js @@ -0,0 +1,89 @@ +'use babel'; +'use strict'; + +import * as fs from 'fs'; + +const SPL_MSG_REGEX = /^([\w.]+(?:\/[\w.]+)?):(\d+):(\d+):\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|INFO):.*)$/; + +const SPL_NAMESPACE_REGEX = /^\s*(?:\bnamespace\b)\s+([a-z|A-Z|0-9|.|_]+)\s*;/gm; + +const SPL_MAIN_COMPOSITE_REGEX = /.*?(?:\bcomposite\b)(?:\s*|\/\/.*?|\/\*.*?\*\/)+([a-z|A-Z|0-9|.|_]+)(?:\s*|\/\/.*?|\/\*.*?\*\/)*\{/gm; + +const BUILD_ACTION = { DOWNLOAD: 0, SUBMIT: 1 }; + +function getFqnMainComposites(selectedFilePath) { + let fileContents = ''; + if (selectedFilePath) { + fileContents = fs.readFileSync(selectedFilePath, 'utf-8'); + } + + // Parse selected SPL file to find namespace and main composites + const namespaces = []; + let m = ''; + while ((m = SPL_NAMESPACE_REGEX.exec(fileContents)) !== null) { namespaces.push(m[1]); } + const mainComposites = []; + while ((m = SPL_MAIN_COMPOSITE_REGEX.exec(fileContents)) !== null) { mainComposites.push(m[1]); } + + let fqn = ''; + let namespace = ''; + if (namespaces && namespaces.length > 0) { + fqn = `${namespaces[0]}::`; + namespace = namespaces[0]; + } + if (mainComposites.length === 1) { + fqn = `${fqn}${mainComposites[0]}`; + } + return { fqn, namespace, mainComposites }; +} + + +/** + * read VCAP_SERVICES env variable, process the file it refers to. + * Expects VCAP JSON format, + * eg: {"streaming-analytics":[{"name":"service-1","credentials":{apikey:...,v2_rest_url:...}}]} + */ +function parseV4ServiceCredentials(streamingAnalyticsCredentials) { + const vcapServicesPath = process.env.VCAP_SERVICES; + if (streamingAnalyticsCredentials && typeof (streamingAnalyticsCredentials) === 'string') { + const serviceCreds = JSON.parse(streamingAnalyticsCredentials); + if (serviceCreds && serviceCreds.apikey && serviceCreds.v2_rest_url) { + return serviceCreds; + } + } else if (vcapServicesPath && typeof (vcapServicesPath) === 'string') { + try { + if (fs.existsSync(vcapServicesPath)) { + const vcapServices = JSON.parse(fs.readFileSync(vcapServicesPath, 'utf8')); + if (vcapServices.apikey && vcapServices.v2_rest_url) { + console.log('vcap:', vcapServices); + return { apikey: vcapServices.apikey, v2_rest_url: vcapServices.v2_rest_url }; + } + const streamingAnalytics = vcapServices['streaming-analytics']; + if (streamingAnalytics && streamingAnalytics[0]) { + const { credentials } = streamingAnalytics[0]; + if (credentials) { + return { apikey: credentials.apikey, v2_rest_url: credentials.v2_rest_url }; + } + console.log('Credentials not found in streaming-analytics service in VCAP'); + } else { + console.log('streaming-analytics service not found in VCAP'); + } + } else { + console.log(`The VCAP file does not exist: ${vcapServicesPath}`); + } + } catch (error) { + console.log(`Error processing VCAP file: ${vcapServicesPath}`, error); + } + } + return {}; +} + +const StreamsUtils = { + SPL_MAIN_COMPOSITE_REGEX, + SPL_MSG_REGEX, + SPL_NAMESPACE_REGEX, + BUILD_ACTION, + parseV4ServiceCredentials, + getFqnMainComposites +}; + +export default StreamsUtils; diff --git a/lib/views/MainCompositePicker.js b/lib/views/MainCompositePicker.js index 40b328c..7ee6705 100644 --- a/lib/views/MainCompositePicker.js +++ b/lib/views/MainCompositePicker.js @@ -1,79 +1,83 @@ -// @flow +'use babel'; +'use strict'; -"use strict"; -"use babel"; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import { Button, ButtonToolbar, Modal } from 'react-bootstrap'; +import CreatableSelect from 'react-select/lib/Creatable'; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { Button, ButtonToolbar, Modal } from "react-bootstrap"; -import CreatableSelect from "react-select/lib/Creatable"; +export default class MainCompositePicker extends React.Component { + mainComposites = []; -export default class MainCompositePicker extends React.Component { + namespace = ''; - mainComposites = []; - namespace = ""; - chosenMainComposite = ""; + chosenMainComposite = ''; - constructor(props: Props){ - super(props); - this.state = {mainComposites: []}; - } + constructor(props) { + super(props); + this.state = { mainComposites: [] }; + } - render(): React.Node { - const style = { - "borderBottomStyle": "solid", - "borderBottomWidth": "1px", - "borderLeftStyle": "solid", - "borderLeftWidth": "1px", - "borderRightStyle": "solid", - "borderRightWidth": "1px", - "fontWeight": "bold", - "padding": "12px" - }; + render() { + const style = { + borderBottomStyle: 'solid', + borderBottomWidth: '1px', + borderLeftStyle: 'solid', + borderLeftWidth: '1px', + borderRightStyle: 'solid', + borderRightWidth: '1px', + fontWeight: 'bold', + padding: '12px' + }; - return( - - - Main Composite - - - ({label: a, value: a}))} - placeholder="Select the main composite to build..." - /> - - - - - - - + const { handleBuild, handleCancel } = this.props; + const { show, mainComposites } = this.state; - - ); - } + return ( + + + Main Composite + + + ({ label: a, value: a }))} + placeholder="Select the main composite to build..." + /> + + + + + + + - handleChange = (newValue, actionMeta) => { - this.setState({mainComposite: newValue}); - if (newValue) { - this.props.handleUpdate(this.state.namespace, newValue.value); - } else { - this.props.handleUpdate("", ""); - } - - } - - handleInputChange = (inputValue, actionMeta) => { - } + + ); + } + handleChange = (newValue, actionMeta) => { + const { handleUpdate } = this.props; + const { namespace } = this.state; + if (newValue) { + handleUpdate(namespace, newValue.value); + } else { + handleUpdate('', ''); + } + } } + +MainCompositePicker.propTypes = { + handleUpdate: PropTypes.func.isRequired, + handleBuild: PropTypes.func.isRequired, + handleCancel: PropTypes.func.isRequired +}; diff --git a/lib/views/MainCompositePickerView.js b/lib/views/MainCompositePickerView.js index 8adf114..f37e648 100644 --- a/lib/views/MainCompositePickerView.js +++ b/lib/views/MainCompositePickerView.js @@ -1,62 +1,62 @@ -// @flow - -"use babel"; -"use strict"; - -import { CompositeDisposable, Emitter } from "atom"; - -import React from "react"; -import ReactDOM from "react-dom"; -import MainCompositePicker from "./MainCompositePicker"; - -export class MainCompositePickerView { - - handleBuild; - handleCancel; - mainComposite: string; - namespace: string; - picker: MainCompositePicker; - - constructor(handleBuild, handleCancel) { - this.element = document.createElement("div"); - this.element.classList.add("main-composite-picker"); - this.handleBuild = handleBuild; - this.handleCancel = handleCancel; - this.render(); - } - - render() { - ReactDOM.render( - this.picker = picker} - handleUpdate={this.updateSelectedValue.bind(this)} - handleBuild={this.handleBuild} - handleCancel={this.handleCancel} - />, this.element); - } - - /** - * callback to keep track of user selection in the picker component. - */ - updateSelectedValue(namespace, mainComposite) { - this.namespace = namespace; - this.mainComposite = mainComposite; - } - - /** - * Update picker component state with the parsed namespace and main composites - */ - updatePickerContent(namespace, mainComposites) { - this.picker.setState({namespace: namespace, mainComposites: mainComposites}); - } - - destroy() { - ReactDOM.unmountComponentAtNode(this.element); - this.element.remove(); - } - - getElement() { - return this.element; - } - -}; +'use babel'; +'use strict'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import MainCompositePicker from './MainCompositePicker'; + +export default class MainCompositePickerView { + handleBuild; + + handleCancel; + + mainComposite: string; + + namespace: string; + + picker: MainCompositePicker; + + constructor(handleBuild, handleCancel) { + this.element = document.createElement('div'); + this.element.classList.add('main-composite-picker'); + this.element.classList.add('native-key-bindings'); + this.handleBuild = handleBuild; + this.handleCancel = handleCancel; + this.render(); + } + + render() { + ReactDOM.render( + this.picker = picker} + handleUpdate={this.updateSelectedValue.bind(this)} + handleBuild={this.handleBuild} + handleCancel={this.handleCancel} + />, this.element + ); + } + + /** + * callback to keep track of user selection in the picker component. + */ + updateSelectedValue(namespace, mainComposite) { + this.namespace = namespace; + this.mainComposite = mainComposite; + } + + /** + * Update picker component state with the parsed namespace and main composites + */ + updatePickerContent(namespace, mainComposites) { + this.picker.setState({ namespace, mainComposites }); + } + + destroy() { + ReactDOM.unmountComponentAtNode(this.element); + this.element.remove(); + } + + getElement() { + return this.element; + } +} diff --git a/lib/views/icp4dAuth/Icp4dAuthenticationView.js b/lib/views/icp4dAuth/Icp4dAuthenticationView.js new file mode 100644 index 0000000..5fd5223 --- /dev/null +++ b/lib/views/icp4dAuth/Icp4dAuthenticationView.js @@ -0,0 +1,40 @@ +'use babel'; +'use strict'; + +// import { CompositeDisposable, Emitter } from 'atom'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import Wizard from './components/Wizard'; + +export default class Icp4dAuthenticationView { + constructor(getStore, closePanel) { + this.element = document.createElement('div'); + this.element.classList.add('icp4d-authentication'); + this.element.classList.add('native-key-bindings'); + this.getStore = getStore; + this.closePanel = closePanel; + this.render(); + } + + render() { + ReactDOM.render( + +
+

IBM Cloud Private for Data Settings

+
+ +
+
, + this.element + ); + } + + destroy() { + ReactDOM.unmountComponentAtNode(this.element); + } + + getElement() { + return this.element; + } +} diff --git a/lib/views/icp4dAuth/components/Step1.js b/lib/views/icp4dAuth/components/Step1.js new file mode 100644 index 0000000..9cc37e6 --- /dev/null +++ b/lib/views/icp4dAuth/components/Step1.js @@ -0,0 +1,288 @@ +'use babel'; +'use strict'; + +import * as React from 'react'; +import { + Alert, Button, Form, ButtonToolbar +} from 'react-bootstrap'; +import ReactLoading from 'react-loading'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + authenticateIcp4d, + setFormDataField, + setIcp4dAuthError +} from '../../../actions'; +import StateSelector from '../../../util/state-selectors'; +import MessageHandlerRegistry from '../../../message-handler-registry'; + +const errorInputStyle = { + borderColor: '#b78e92', + boxShadow: '0 0 0 0.2rem rgba(234,151,159,.25' +}; + +const buttonBarStyle = { + display: 'flex', + width: '100%' +}; + +class Step1 extends React.Component { + constructor(props) { + super(props); + + this.state = { + isAuthenticating: false, + touched: { + username: false, + password: false + } + }; + } + + onTextChange = (e) => { + const { updateFormDataField } = this.props; + updateFormDataField(e.target.name, e.target.value); + } + + onCheckboxChange = (e) => { + const { updateFormDataField } = this.props; + updateFormDataField(e.target.name, e.target.checked); + } + + onBlur = (e) => { + const { touched } = this.state; + this.setState({ + touched: { + ...touched, + [e.target.name]: true + } + }); + } + + static getDerivedStateFromProps(props, currentState) { + const { currentStep } = props; + if (currentStep !== 1) { // if we end up on a different step, we have authenticated + return ({ isAuthenticating: false }); + } + return null; + } + + renderErrorHeader = () => { + const { icp4dAuthError } = this.props; + if (!icp4dAuthError) { + return null; + } + switch (icp4dAuthError) { + case 401: + return ( + + Incorrect username or password. + + ); + default: + return ( + + An error occurred while authenticating. + + ); + } + } + + renderLoadingSpinner = () => { + const { icp4dAuthError } = this.props; + const { isAuthenticating } = this.state; + return (isAuthenticating && !icp4dAuthError) ? ( + + ) : null; + } + + validate = (username, password) => ({ + username: username.length === 0, + password: password.length === 0 + }); + + showError = (errors, touched, field) => { + const hasError = errors[field]; + const shouldShow = touched[field]; + return hasError ? shouldShow : false; + }; + + renderIcp4dUrlNotSetError = () => { + const { icp4dUrl, closePanel } = this.props; + if (!icp4dUrl) { + return ( + + IBM Cloud Private for Data URL not specified. Go to build-ibmstreams package settings to specify it. + + + ); + } + } + + render() { + const { + currentStep, + username, + password, + rememberPassword, + closePanel, + setAuthError, + authenticate, + icp4dUrl + } = this.props; + + const { + touched + } = this.state; + + if (currentStep !== 1) { + return null; + } + + const errors = this.validate(username, password); + + const isEnabled = !Object.keys(errors).some(field => errors[field]) && icp4dUrl; + + return ( +
+ {this.renderErrorHeader()} + + {this.renderIcp4dUrlNotSetError()} + + {this.renderLoadingSpinner()} + +
+ + Username + this.usernameInput = e} + onBlur={this.onBlur} + onChange={this.onTextChange} + value={username} + /> + + + + Password + + + + + + + + + + + + +
+
+ ); + } +} + +const mapStateToProps = (state) => { + let username = StateSelector.getFormUsername(state); + if (typeof username !== 'string') { + username = StateSelector.getUsername(state) || ''; + } + let rememberPassword = StateSelector.getFormRememberPassword(state); + if (typeof rememberPassword !== 'boolean') { + rememberPassword = StateSelector.getRememberPassword(state); + if (typeof rememberPassword !== 'boolean') { + rememberPassword = true; + } + } + const password = StateSelector.getFormPassword(state) || ''; + + return { + icp4dUrl: StateSelector.getIcp4dUrl(state) || null, + loginFormInitialized: StateSelector.getLoginFormInitialized(state) || false, + icp4dAuthError: StateSelector.getIcp4dAuthError(state) || null, + username, + password, + rememberPassword, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + authenticate: (username, password, rememberPassword) => dispatch(authenticateIcp4d(username, password, rememberPassword)), + updateFormDataField: (key, value) => dispatch(setFormDataField(key, value)), + setAuthError: (authError) => dispatch(setIcp4dAuthError(authError)) + }; +}; + +Step1.defaultProps = { + icp4dAuthError: null +}; + +Step1.propTypes = { + closePanel: PropTypes.func.isRequired, + authenticate: PropTypes.func.isRequired, + updateFormDataField: PropTypes.func.isRequired, + icp4dAuthError: PropTypes.number, + setAuthError: PropTypes.func.isRequired, + + icp4dUrl: PropTypes.string.isRequired, + username: PropTypes.string.isRequired, + password: PropTypes.string.isRequired, + rememberPassword: PropTypes.bool.isRequired, + currentStep: PropTypes.number.isRequired +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Step1); diff --git a/lib/views/icp4dAuth/components/Step2.js b/lib/views/icp4dAuth/components/Step2.js new file mode 100644 index 0000000..666bc90 --- /dev/null +++ b/lib/views/icp4dAuth/components/Step2.js @@ -0,0 +1,135 @@ +'use babel'; +'use strict'; + +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { Button, ButtonToolbar, Form } from 'react-bootstrap'; +import { ReactLoading } from 'react-loading'; +import Select from 'react-select'; +import { connect } from 'react-redux'; +import { setSelectedInstance, setCurrentLoginStep } from '../../../actions'; +import StateSelector from '../../../util/state-selectors'; + +const buttonBarStyle = { + display: 'flex', + width: '100%' +}; + +class Step2 extends React.Component { + constructor(props) { + super(props); + + this.state = { + localSelection: null + }; + } + + onInstanceSelectionChange = (selectedInstance) => { + this.setState({ localSelection: selectedInstance }); + } + + setInstanceSelection = () => { + const { setInstance } = this.props; + const { localSelection } = this.state; + setInstance(localSelection); + } + + renderLoadingSpinner = () => { + const { isAuthenticating } = this.state; + return isAuthenticating ? ( + + ) : null; + } + + render() { + const { + currentStep, + streamsInstances, + setCurrentStep, + closePanel + } = this.props; + + const { + localSelection + } = this.state; + + if (currentStep !== 2) { + return null; + } + + return ( +
+ {this.renderLoadingSpinner()} +
+ + Streams instance +