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..83d5a3e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,56 @@ +{ + "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", + "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": [ + "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/.flowconfig b/.flowconfig deleted file mode 100644 index c83a24c..0000000 --- a/.flowconfig +++ /dev/null @@ -1,21 +0,0 @@ -[ignore] -.*/node_modules/jsonlint/**/.*.json - -[include] - -[lints] -all=warn - -[libs] -# Include flow-typed under your application folder -flow-typed - -[options] - -esproposal.export_star_as=enable - -module.file_ext=.js -module.file_ext=.json -module.file_ext=.jsx -module.file_ext=.css -module.file_ext=.scss 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/README.md b/README.md index d56af67..92283ff 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,28 @@ This is the initial public release. For best results you should also install th * ide-ibmstreams ### Setup Instructions -#### Build - Streaming Analytics Credentials -The build-ibmstreams package requires a running IBM Streaming Analytics service. SPL applications will be built and deployed on this service. If you need to create one, start here and follow the instructions to create an account. + +### Build + +You may either build and run your Streams applications on an [IBM Cloud Streaming Analytics service](https://cloud.ibm.com/docs/services/StreamingAnalytics/index.html#gettingstarted) (V4.3) or an [IBM Cloud Private for Data (ICP4D) Streams add-on instance](https://www.ibm.com/support/knowledgecenter/SSQNUZ_current/com.ibm.icpdata.doc/streams/intro.html) (V5). + +#### IBM Cloud: Streaming Analytics service +A running IBM Streaming Analytics service is required. You must provide your service credentials (in JSON format) in order for this extension to connect to your service. SPL applications will be built and deployed on this service. If you need to create one, start [here](https://cloud.ibm.com/catalog/services/streaming-analytics) and follow the instructions to create an account. Note:The service needs to support V2 of the rest api. -Once you have an account go to your dashboard and select the Streaming Analytics service you want to use. You need to make sure it is running and then copy your credentials to the clipboard. To get your credentials select Service Credentials from the actions on the left. From the credentials page, press View credentials for the one you want to use and press the copy button in the upper right side of the credentials to copy them to the clipboard. +Once you have an account go to your [Dashboard](https://cloud.ibm.com/resources?groups=resource-instance) and select the Streaming Analytics service you want to use. Ensure that it is running and then create a new set of credentials. Select the __Service credentials__ tab on the left and select your existing credentials or click on the __New credential__ button. On the credentials page, click on the __View credentials__ action and click on the __Copy__ button in the top-right corner of the credentials snippet to copy them to the clipboard. -In Atom there is a setting in the build-ibmstreams package for the credentials. Go to Atom->Preferences->Packages and press the Settings button on the build-ibmstreams package and paste your credentials into the setting. +In Atom, to open __build-ibmstreams__ package settings go to Atom->Preferences->Packages and press the Settings button on the build-ibmstreams package. Select __IBM Cloud Streaming Analytics service__ in the __Build and submit__ dropdown and then paste your credentials in the __Streaming Analytics Credentials__ field. ![](./images/atomcredssetting.png) +#### IBM Cloud Private for Data: Streams add-on instance + +A provisioned IBM Streams add-on is required. You must provide your IBM Cloud Private for Data URL in order for this extension to connect to your add-on instance. Enter your url in the __build-ibmstreams__ package settings and select __IBM Cloud Private for Data Streams addon__ in the __Build and submit__ dropdown. + +If you need to provision an add-on, start [here](https://www.ibm.com/support/knowledgecenter/SSQNUZ_current/com.ibm.icpdata.doc/streams/intro.html) and follow the instructions. + + ### SPL Application build ![](./images/build.gif) diff --git a/lib/LintHandler.js b/lib/LintHandler.js index cb15e3f..203c4fe 100644 --- a/lib/LintHandler.js +++ b/lib/LintHandler.js @@ -1,78 +1,101 @@ -// @flow -"use strict"; -"use babel"; +'use babel'; +'use strict'; -export class LintHandler { +import { StreamsUtils } from './util'; - linter = null; - msgRegex = null; - appRoot = null; +const CONF_API_VERSION_V4 = 'v4'; +const CONF_API_VERSION_V5 = 'v5'; - constructor(linter, msgRegex, appRoot) { - this.linter = linter; - this.msgRegex = msgRegex; - this.appRoot = appRoot; - } +export default class LintHandler { + linter = null; + msgRegex = null; - lint(input) { - if (!this.linter || !input) { - return; - } + appRoot = null; - 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]}`; - } + apiVersion = null; - return { - severity: severity, - location: { + constructor(linter, appRoot, apiVersion) { + this.linter = linter; + this.appRoot = appRoot; + this.apiVersion = apiVersion; + this.msgRegex = apiVersion === CONF_API_VERSION_V4 ? StreamsUtils.SPL_MSG_REGEX : StreamsUtils.SPL_MSG_REGEX_V5; + } - 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], - }; - } - ); + setV5() { + this.apiVersion = CONF_API_VERSION_V5; + this.msgRegex = StreamsUtils.SPL_MSG_REGEX_V5; + } - this.linter.setAllMessages(convertedMessages); + setV4() { + this.apiVersion = CONF_API_VERSION_V4; + this.msgRegex = StreamsUtils.SPL_MSG_REGEX; + } - if (Array.isArray(convertedMessages) && convertedMessages.length > 0) { - atom.workspace.open("atom://nuclide/diagnostics"); - } - } - } + lint(input) { + if (!this.linter || !input) { + return; + } + let messages = []; + if (input.output) { + this.setV4(); + messages = input.output.map(message => message.message_text); + } else if (Array.isArray(input)) { + this.setV5(); + messages = input; + } + + if (Array.isArray(messages)) { + const convertedMessages = messages.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); + if (parts && parts.length > 4) { + 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]}`; + } + return { + severity, + location: { + 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..81136e1 100644 --- a/lib/MessageHandler.js +++ b/lib/MessageHandler.js @@ -1,153 +1,183 @@ -// @flow - -"use strict"; -"use babel"; - -export class MessageHandler { - consoleService: null; - - 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); - - 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); - - 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); - - 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); - atom.workspace.open("atom://config/packages/build-ibmstreams"); - } - }] - } - ); - return n; - } - - processButtons(btns) { - let buttons = {}; - if (Array.isArray(btns)) { - buttons.buttons = btns.map(obj => ({onDidClick: obj.callbackFn, text: obj.label})); - } - return buttons; - } - - joinMessageArray(msgArray) { - if (Array.isArray(msgArray)) { - return msgArray.join("\n").trimRight(); - } - return msgArray; - } - - dismissNotification(notification) { - if (notification && typeof(notification.dismiss) === "function") { - notification.dismiss(); - } - } - - getLoggableMessage(messages: Array) { - return this.joinMessageArray(messages.map(outputMsg => outputMsg.message_text)); - } +'use babel'; +'use strict'; + +export default class MessageHandler { + consoleService: null; + + 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); + + 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); + } + } + + 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); + } + } + + 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); + } + } + + 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; + } + + handleIcp4dUrlNotSet() { + const notification = this.handleError('IBM Cloud Private for Data URL is not specified, is invalid, or is unreachable', + { + 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(); + } + } + ] + }); + } + + openPackageSettingsPage() { + atom.workspace.open('atom://config/packages/build-ibmstreams'); + } + + openIdePackageSettingsPage() { + atom.workspace.open('atom://config/packages/ide-ibmstreams'); + } + + processButtons(btns) { + const buttons = {}; + if (Array.isArray(btns)) { + buttons.buttons = btns.map(obj => ({ onDidClick: obj.callbackFn, text: obj.label })); + } + return buttons; + } + + joinMessageArray(msgArray) { + if (Array.isArray(msgArray)) { + return msgArray.join('\n').trimRight(); + } + return msgArray; + } + + dismissNotification(notification) { + if (notification && typeof (notification.dismiss) === 'function') { + notification.dismiss(); + } + } + + getLoggableMessage(messages: Array) { + return this.joinMessageArray(messages.map(outputMsg => outputMsg.message_text)); + } + + setSubmitStartedNotification(notification) { + this.submitStartedNotification = notification; + } + + closeSubmitStartedNotification() { + this.dismissNotification(this.submitStartedNotification); + } } diff --git a/lib/actions/index.js b/lib/actions/index.js new file mode 100644 index 0000000..ece6019 --- /dev/null +++ b/lib/actions/index.js @@ -0,0 +1,304 @@ +'use babel'; +'use strict'; + +/* eslint-disable import/prefer-default-export */ + +export const packageActivated = () => ({ + type: actions.PACKAGE_ACTIVATED +}); + +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 checkIcp4dHostExists = (successFn, errorFn) => ({ + type: actions.CHECK_ICP4D_HOST_EXISTS, + successFn, + errorFn +}); + +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 resetAuth = () => ({ + type: actions.RESET_AUTH +}); + +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', + 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', + CHECK_ICP4D_HOST_EXISTS: 'CHECK_ICP4D_HOST_EXISTS', + 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', + RESET_AUTH: 'RESET_AUTH', + + QUEUE_ACTION: 'QUEUE_ACTION', + CLEAR_QUEUED_ACTION: 'CLEAR_QUEUED_ACTION', + + NEW_BUILD: 'NEW_BUILD', + START_BUILD: 'START_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_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', + + POST_PACKAGE_ACTIVATED: 'POST_PACKAGE_ACTIVATED', + POST_ERROR: 'POST_ERROR', + POST_CHECK_ICP4D_HOST_EXISTS: 'POST_CHECK_ICP4D_HOST_EXISTS', + POST_GET_BUILD_ARTIFACTS_FULFILLED: 'POST_GET_BUILD_ARTIFACTS_FULFILLED', + POST_DOWNLOAD_ARTIFACTS: 'POST_DOWNLOAD_ARTIFACTS', + POST_SUBMIT_APPLICATIONS: 'POST_SUBMIT_APPLICATIONS', + POST_SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES: 'POST_SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES', + POST_OPEN_STREAMS_CONSOLE: 'POST_OPEN_STREAMS_CONSOLE', + POST_REFRESH_TOOLKITS: 'POST_REFRESH_TOOLKITS', + +}; diff --git a/lib/epics/index.js b/lib/epics/index.js new file mode 100644 index 0000000..47fc5a1 --- /dev/null +++ b/lib/epics/index.js @@ -0,0 +1,505 @@ +'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, + ResponseSelector, + StreamsRestUtils, + SourceArchiveUtils, + StatusUtils, + StreamsToolkitsUtils, + KeychainUtils +} from '../util'; +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), + withLatestFrom(state), + mergeMap(([uploadAction, s]) => SourceArchiveUtils.buildSourceArchive({ + buildId: uploadAction.buildId, + appRoot: uploadAction.appRoot, + toolkitPathSetting: uploadAction.toolkitRootPath, + toolkitCacheDir: StateSelector.getToolkitsCacheDir(s), + 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( + 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: actions.POST_GET_BUILD_ARTIFACTS_FULFILLED })) +); + +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: actions.POST_DOWNLOAD_ARTIFACTS })), + 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); + }) + )), + map(() => ({ type: actions.POST_SUBMIT_APPLICATIONS })), + 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); + }), + map(() => ({ type: actions.POST_SUBMIT_APPLICATIONS_FROM_BUNDLE_FILES })) + ); + }) + )), + 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: actions.POST_OPEN_STREAMS_CONSOLE })) +); + +const icp4dHostExistsEpic = (action, state) => action.pipe( + ofType(actions.CHECK_ICP4D_HOST_EXISTS), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.icp4d.icp4dHostExists(s).pipe( + tap((response) => a.successFn()), + catchError(error => { + a.errorFn(); + return of(handleError(a, error)); + }) + )), + map(() => ({ type: actions.POST_CHECK_ICP4D_HOST_EXISTS })) +); + +const icp4dAuthEpic = (action, state) => action.pipe( + ofType(actions.AUTHENTICATE_ICP4D), + withLatestFrom(state), + mergeMap(([a, s]) => StreamsRestUtils.icp4d.getIcp4dToken(s, a.username, a.password).pipe( + 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( + mergeMap(authTokenResponse => { + const statusCode = ResponseSelector.getStatusCode(authTokenResponse); + if (statusCode === 200) { + const queuedActionObservable = StateSelector.getQueuedAction(s) ? merge( + of(StateSelector.getQueuedAction(s)), // if there was a queued action, run it now... + of(clearQueuedAction()) + ) : of(); + return merge( + of(setStreamsAuthToken(ResponseSelector.getStreamsAuthToken(authTokenResponse))), + of(setStreamsAuthError(false)), + of(refreshToolkits()), + queuedActionObservable, + authDelayObservable().pipe( + tap(() => { + console.log('reauthenticating to streams instance'); + }), + mergeMap(() => of(authenticateStreamsInstance(authAction.instanceName))), + catchError(error => of(handleError(authAction, error))) + ) + ); + } + return of(setStreamsAuthError(true)); + }), + 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 => StreamsToolkitsUtils.getToolkitsToCache(s, toolkits)), + mergeMap(toolkitsToCache => forkJoin(from(toolkitsToCache).pipe( + mergeMap(toolkitToCache => StreamsRestUtils.toolkit.getToolkitIndex(s, toolkitToCache.id).pipe( + map(toolkitIndexResponse => StreamsToolkitsUtils.cacheToolkitIndex(s, toolkitToCache, toolkitIndexResponse.body)) + )), + defaultIfEmpty('empty') + ))), + tap(() => StreamsToolkitsUtils.refreshLspToolkits(s, MessageHandlerRegistry.sendLspNotification)), + map(() => ({ type: actions.POST_REFRESH_TOOLKITS })), + 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: actions.POST_PACKAGE_ACTIVATED }; + }) +); + +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: `Error occurred during ${a.sourceAction.type}`, stack: a.error.stack }); + } + }), + map(() => ({ type: actions.POST_ERROR })) +); + +const rootEpic = combineEpics( + errorHandlingEpic, + + buildAppEpic, + buildStatusEpic, + uploadSourceEpic, + sourceArchiveCreatedEpic, + startBuildEpic, + buildStatusLoopEpic, + + getBuildArtifactsEpic, + getBuildArtifactsFulfilledEpic, + downloadArtifactsEpic, + submitApplicationsEpic, + submitApplicationsFromBundleFilesEpic, + + openStreamsConsoleEpic, + + instanceSelectedEpic, + icp4dHostExistsEpic, + 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..b9db497 --- /dev/null +++ b/lib/reducers/index.js @@ -0,0 +1,246 @@ +'use babel'; +'use strict'; + +import * as _ from 'lodash'; +import { combineReducers } from 'redux'; +import { actions } from '../actions'; + +const streamsV5Build = (state = [], action) => { + if (atom.inDevMode()) { + 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.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.RESET_AUTH: + return { + ..._.omit(state, [ + 'currentLoginStep', + 'icp4dAuthToken', + 'icp4dAuthError', + 'streamsInstances', + 'selectedInstance', + 'streamsAuthError', + 'username' + ]), + currentLoginStep: 1 + }; + 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..3ed89db --- /dev/null +++ b/lib/redux-store/configure-store.js @@ -0,0 +1,40 @@ +'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 = atom.inDevMode() && composeWithDevTools ? composeWithDevTools({ hostname: 'localhost', port: 8000, realtime: true }) : compose; + +const addLoggingToDispatch = (s) => { + const rawDispatch = s.dispatch; + return (action) => { + if (atom.inDevMode()) { + 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..1a0eb7c 100644 --- a/lib/spl-build-common.js +++ b/lib/spl-build-common.js @@ -1,950 +1,815 @@ -// @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 }); + +function setTimeout(timeoutInSeconds) { + request.defaults.timeout = timeoutInSeconds * 1000; +} +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 {}; + } } + +const SplBuildCommonV4 = { + setTimeout +}; + +export default SplBuildCommonV4; diff --git a/lib/spl-build.js b/lib/spl-build.js index 5ba312c..3406395 100644 --- a/lib/spl-build.js +++ b/lib/spl-build.js @@ -1,346 +1,911 @@ -// @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 _ from 'lodash'; + +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 SplBuildCommonV4, { SplBuilder } from './spl-build-common'; +import MainCompositePickerView from './views/MainCompositePickerView'; +import Icp4dAuthenticationView from './views/icp4dAuth/Icp4dAuthenticationView'; + +import { + StateSelector, + KeychainUtils, + StreamsUtils, + SourceArchiveUtils, + StreamsToolkitsUtils, + StreamsRestUtils +} from './util'; +import { + setIcp4dUrl, + queueAction, + newBuild, + submitApplicationsFromBundleFiles, + setToolkitsCacheDir, + setToolkitsPathSetting, + setUsername, + setRememberPassword, + setFormDataField, + setBuildOriginator, + setUseIcp4dMasterNodeHost, + resetAuth, + packageActivated, + refreshToolkits, + checkIcp4dHostExists +} from './actions'; +import getStore from './redux-store/configure-store'; + +import { version } from '../package.json'; + +const CONF_TOOLKITS_PATH = 'ide-ibmstreams.toolkitsPath'; +const CONF_STREAMING_ANALYTICS_CREDENTIALS = 'build-ibmstreams.streamingAnalytics.credentials'; +const CONF_ICP4D_URL = 'build-ibmstreams.icp4d.url'; +const CONF_USE_ICP4D_MASTER_NODE_HOST = 'build-ibmstreams.icp4d.useMasterNodeHost'; +const CONF_API_VERSION = 'build-ibmstreams.buildApiVersion'; +const CONF_API_VERSION_V4 = 'v4'; +const CONF_API_VERSION_V5 = 'v5'; +const CONF_REQUEST_TIMEOUT = 'build-ibmstreams.requestTimeout'; + +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() + config: { + buildApiVersion: { + title: 'Build and submit system', + type: 'string', + default: CONF_API_VERSION_V4, + order: 0, + 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 Add-on' } + ] + }, + requestTimeout: { + title: 'Request timeout (seconds)', + type: 'integer', + default: 30, + order: 1, + description: 'Number of seconds before a request times out' + }, + streamingAnalytics: { + type: 'object', + title: 'IBM Cloud Streaming Analytics', + order: 3, + properties: { + credentials: { + title: 'IBM Streaming Analytics Credentials', + type: 'string', + default: '', + description: 'Credentials for an IBM Streaming Analytics service.' } - ) - ); - - 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( + } + }, + icp4d: { + type: 'object', + title: 'IBM Cloud Private for Data Streams Add-on', + order: 2, + properties: { + url: { + 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)' + }, + useMasterNodeHost: { + 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)); + + // migrate settings if old config is set + const streamingAnalyticsOld = 'build-ibmstreams.streamingAnalyticsCredentials'; + if (atom.config.get(streamingAnalyticsOld)) { + atom.config.set(CONF_STREAMING_ANALYTICS_CREDENTIALS, atom.config.get(streamingAnalyticsOld)); + atom.config.unset(streamingAnalyticsOld); + } + + // Atom config listeners + this.subscriptions.add( + atom.config.onDidChange(CONF_ICP4D_URL, {}, (event) => { + try { + // clear any currently saved auth details + getStore().dispatch(resetAuth()); + 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; + }) + ); + this.subscriptions.add( + atom.config.onDidChange(CONF_REQUEST_TIMEOUT, {}, (event) => { + StreamsRestUtils.setTimeout(event.newValue); + SplBuildCommonV4.setTimeout(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))); + } + const timeout = atom.config.get(CONF_REQUEST_TIMEOUT); + StreamsRestUtils.setTimeout(timeout); + SplBuildCommonV4.setTimeout(timeout); + + + this.storeSubscription = getStore().subscribe(() => { + if (atom.inDevMode()) { + 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) { + if (state.username) { + getStore().dispatch(setUsername(state.username)); + } + if (state.rememberPassword) { + getStore().dispatch(setRememberPassword(state.rememberPassword)); + } + } + + getStore().dispatch(setBuildOriginator('atom', version)); + + getStore().dispatch(packageActivated()); + }, + + 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() + }), + atom.commands.add('atom-workspace', 'spl-build:list-toolkits', { + displayName: 'IBM Streams: List available toolkits', + description: 'List available Streams toolkits', + didDispatch: () => this.listToolkits() + }), + ); + }, + + 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); + 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) { + const fqn = this.namespace ? `${this.namespace}::${selectedComp}` : `${selectedComp}`; + 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, this.apiVersion); + LintHandlerRegistry.add(this.appRoot, 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, this.appRoot, this.apiVersion); + this.splBuilder = new SplBuilder(messageHandler, this.lintHandler, this.openUrlHandler, { originator: 'atom', version, type: 'spl' }, { appRoot: this.appRoot, fqn }); + + try { + SourceArchiveUtils.buildSourceArchive( + { + appRoot: this.appRoot, + toolkitPathSetting: 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) { + this.handleV5Action(() => this.buildMakeV5(action)); + } 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, this.appRoot, this.apiVersion); + 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, + toolkitPathSetting: 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, this.apiVersion); + LintHandlerRegistry.add(appRoot, 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) { + this.handleV5Action(() => this.buildAppV5(action)); + } 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, this.appRoot, this.apiVersion); + this.splBuilder = new SplBuilder(messageHandler, this.lintHandler, this.openUrlHandler, { originator: 'atom', version, type: 'spl' }, { appRoot: this.appRoot, fqn }); + try { + SourceArchiveUtils.buildSourceArchive( + { + appRoot: this.appRoot, + toolkitPathSetting: 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, this.apiVersion); + LintHandlerRegistry.add(appRoot, 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, this.appRoot, this.apiVersion); + 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) { + this.handleV5Action(() => this.submitV5()); + } 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) { + const openConsoleFn = () => { + const consoleUrlString = StateSelector.getStreamsConsoleUrl(getStore().getState()); + if (consoleUrlString) { + try { + const consoleUrl = new URL(consoleUrlString); /* eslint-disable-line compat/compat */ + MessageHandlerRegistry.openUrl(`${consoleUrl}`); + } catch (err) { /* */ } + } + }; + this.handleV5Action(openConsoleFn); + } 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, this.appRoot, this.apiVersion); + 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 openDashboard = () => { + try { + const icp4dUrl = new URL(StateSelector.getIcp4dUrl(getStore().getState())); /* eslint-disable-line compat/compat */ + MessageHandlerRegistry.openUrl(`${icp4dUrl}/zen/#/homepage`); + } catch (err) { /* */ } + }; + this.handleV5Action(openDashboard); + } + }, + + listToolkits() { + const cachedToolkits = StreamsToolkitsUtils.getCachedToolkits(StateSelector.getToolkitsCacheDir(getStore().getState())).map(tk => tk.label); + const cachedToolkitsStr = `Build service toolkits:\n\n${cachedToolkits.join('\n')}`; + + const localToolkitsPathSetting = atom.config.get(CONF_TOOLKITS_PATH); + let localToolkitsStr = ''; + if (localToolkitsPathSetting && localToolkitsPathSetting.length > 0) { + const localToolkits = StreamsToolkitsUtils.getLocalToolkits(localToolkitsPathSetting).map(tk => tk.label); + localToolkitsStr = `\n\nLocal toolkits from ${localToolkitsPathSetting}:\n\n${localToolkits.join('\n')}`; + } + MessageHandlerRegistry.getDefault().handleInfo( + 'Streams Toolkits', + { + detail: `${cachedToolkitsStr}${localToolkitsStr}`, + notificationAutoDismiss: false + } + ); + }, + + handleV5Action(callbackFn) { + const icp4dUrl = StateSelector.getIcp4dUrl(getStore().getState()); + if (icp4dUrl) { + const successFn = callbackFn; + const errorFn = () => this.handleIcp4dUrlNotSet(this.handleV5Action.bind(this, callbackFn)); + getStore().dispatch(checkIcp4dHostExists(successFn, errorFn)); + } else { + this.handleIcp4dUrlNotSet(this.handleV5Action.bind(this, callbackFn)); + } + }, + + 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); + getStore().dispatch(setToolkitsPathSetting(toolkitsDirectory)); + } + }, + + handleStreamsBuildUri(parsedUri) { + if (parsedUri.host === 'build-ibmstreams') { + // Handle a toolkit refresh request + if (parsedUri.pathname === '/toolkits/refresh') { + // MessageHandlerRegistry.getDefault().handleInfo('Refreshing toolkits'); + const toolkitPathSetting = atom.config.get(CONF_TOOLKITS_PATH); + if (typeof toolkitPathSetting === 'string' && toolkitPathSetting.length > 0) { + if (toolkitPathSetting.match(/[,;]/)) { + const directories = toolkitPathSetting.split(/[,;]/); + const directoriesInvalid = _.some(directories, dir => !fs.existsSync(dir)); + if (directoriesInvalid) { + MessageHandlerRegistry.getDefault().handleError( + 'One or more toolkit paths do not exist or are not valid', + { + detail: `Verify that the paths exist:\n${directories.join('\n')}`, + notificationButtons: [ + { + label: 'Open settings', + callbackFn: () => MessageHandlerRegistry.getDefault().openIdePackageSettingsPage() + } + ] + } + ); + return; + } + } else if (!fs.existsSync(toolkitPathSetting)) { + MessageHandlerRegistry.getDefault().handleError( + 'The specified toolkit path does not exist or is not valid', + { + detail: `Verify that the specified toolkit path ${toolkitPathSetting} exists.`, + notificationButtons: [ + { + label: 'Open settings', + callbackFn: () => MessageHandlerRegistry.getDefault().openIdePackageSettingsPage() + } + ] + } + ); + return; + } + getStore().dispatch(setToolkitsPathSetting(toolkitPathSetting)); + } + if (StateSelector.hasAuthenticatedToStreamsInstance(getStore().getState())) { + getStore().dispatch(refreshToolkits()); + } + const toolkitInitOptions = StreamsToolkitsUtils.getLangServerOptionForInitToolkits(StateSelector.getToolkitsCacheDir(getStore().getState()), StateSelector.getToolkitsPathSetting(getStore().getState())); + MessageHandlerRegistry.sendLspNotification(toolkitInitOptions); + } + } + }, }; diff --git a/lib/util/index.js b/lib/util/index.js new file mode 100644 index 0000000..2c8d0f7 --- /dev/null +++ b/lib/util/index.js @@ -0,0 +1,11 @@ +'use babel'; +'use strict'; + +export { default as ResponseSelector } from './rest-v5-response-selector'; +export { default as StateSelector } from './state-selectors'; +export { default as SourceArchiveUtils } from './source-archive-utils'; +export { default as StatusUtils } from './status-utils'; +export { default as StreamsRestUtils } from './streams-rest-v5'; +export { default as StreamsToolkitsUtils } from './streams-toolkits-utils'; +export { default as StreamsUtils } from './streams-utils'; +export { default as KeychainUtils } from './keychain-utils'; 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..a57601f --- /dev/null +++ b/lib/util/rest-v5-response-selector.js @@ -0,0 +1,130 @@ +'use babel'; +'use strict'; + +import * as _ from 'lodash'; +/* eslint-disable import/prefer-default-export */ + + +const getBody = (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)) { + 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..34579c3 --- /dev/null +++ b/lib/util/source-archive-utils.js @@ -0,0 +1,224 @@ +'use babel'; +'use strict'; + +import * as path from 'path'; +import * as fs from 'fs'; +import * as _ from 'lodash'; +import * as xmldoc from 'xmldoc'; +import * as semver from 'semver'; + +import { from } from 'rxjs'; +import { actions } from '../actions'; +import MessageHandlerRegistry from '../message-handler-registry'; +import { StreamsToolkitsUtils } from '.'; + +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, + toolkitPathSetting, + toolkitCacheDir, + fqn, + makefilePath + } = { + } +) { + 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', () => { + 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(toolkitCacheDir, toolkitPathSetting, appRoot); + let tkPathString = ''; + if (toolkitPaths && toolkitPaths.length > 0) { + messageHandler.handleInfo('Including toolkits in source archive...', + { + detail: `Including the following toolkits with the application source:\n${toolkitPaths.map(tk => tk.tkPath).join('\n')}` + }); + 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 */ + } +} + +function getToolkits(toolkitCacheDir, toolkitPathSetting, appRoot) { + const allToolkits = StreamsToolkitsUtils.getAllToolkits(toolkitCacheDir, toolkitPathSetting); + + // if info.xml exists, only include toolkits that are dependencies, ensuring they are newer versions than those on the build service + if (fs.existsSync(`${appRoot}${path.sep}info.xml`)) { + try { + const xml = fs.readFileSync(`${appRoot}${path.sep}info.xml`, 'utf8'); + const document = new xmldoc.XmlDocument(xml); + const dependenciesNode = document.childNamed('info:dependencies'); + if (dependenciesNode) { + const dependencyToolkitsNodes = dependenciesNode.childrenNamed('info:toolkit'); + if (dependencyToolkitsNodes) { + const dependencies = dependencyToolkitsNodes.map(node => ({ + name: node.valueWithPath('common:name'), + version: node.valueWithPath('common:version') + })); + const newestLocalToolkits = StreamsToolkitsUtils.filterNewestToolkits(allToolkits).filter(tk => tk.isLocal); + const toolkitsToInclude = _.intersectionWith(newestLocalToolkits, dependencies, (tk, dependency) => tk.name === dependency.name && semver.gte(tk.version, dependency.version)); + return toolkitsToInclude.map(tk => ({ name: tk.name, tkPath: path.dirname(tk.indexPath) })); + } + } + } catch (err) { + throw new Error(`Error reading toolkit dependencies from ${appRoot}${path.sep}info.xml\n${err}`); + } + } else { + // if there is no info.xml, include all local toolkits, ensuring they are newer versions than those on the build service + return StreamsToolkitsUtils.filterNewestToolkits(allToolkits).filter(tk => tk.isLocal).map(tk => ({ name: tk.name, tkPath: path.dirname(tk.indexPath) })); + } +} + +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..8fa8a00 --- /dev/null +++ b/lib/util/state-selectors.js @@ -0,0 +1,320 @@ +'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 => 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) => { + try { + 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(); + } catch (err) { + return endpointUrlString; + } +}; + + +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..65fe79d --- /dev/null +++ b/lib/util/status-utils.js @@ -0,0 +1,154 @@ +'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) { + 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); + 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); + if (lintHandler) lintHandler.lint(logMessages); + } else if (buildStatus === 'building') { + messageHandler.handleInfo(`Building ${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) { + 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: () => { + // messageHandler.handleInfo('Submitting application'); + getStore().dispatch(submitApplications(buildId, true)); + } + }, + { + label: 'Submit via Streams Console', + callbackFn: () => { + // messageHandler.handleInfo('Downloading application bundle(s)'); + getStore().dispatch(downloadAppBundles(buildId)); + getStore().dispatch(openStreamingAnalyticsConsole()); + } + } + ] + }); + } + } else { + getStore().dispatch(downloadAppBundles(buildId)); + } + } +} + +function submitJobStart(state, artifactName, buildId) { + const messageHandler = buildId ? getMessageHandlerForBuildId(state, buildId) : MessageHandlerRegistry.getDefault(); + if (messageHandler) { + const submitStartNotification = messageHandler.handleInfo(`Submitting application ${artifactName} to the Streams Instance...`, { notificationAutoDismiss: false }); + messageHandler.setSubmitStartedNotification(submitStartNotification); + } +} + +function jobSubmitted(state, submitInfo, buildId) { + const messageHandler = buildId ? getMessageHandlerForBuildId(state, buildId) : MessageHandlerRegistry.getDefault(); + messageHandler.closeSubmitStartedNotification(); + 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..bb5be33 --- /dev/null +++ b/lib/util/streams-rest-v5.js @@ -0,0 +1,459 @@ +'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, + agentOptions: { + rejectUnauthorized: false + }, + ecdhCurve: 'auto', + strictSSL: false, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, +}; + +const baseRequest = request.defaults(baseRequestOptions); + +function setTimeout(timeoutInSeconds) { + baseRequest.defaults.timeout = timeoutInSeconds * 1000; +} + +/** + * StreamsRestUtils.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); +} + +/** + * StreamsRestUtils.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); +} + +/** + * StreamsRestUtils.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 icp4dHostExists(state) { + const options = { + method: 'HEAD', + url: StateSelector.getIcp4dUrl(state), + timeout: 2000 + }; + return observableRequest(baseRequest, options); +} + +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) { + if (atom.inDevMode()) { + 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 = { + icp4dHostExists, + getServiceInstances, + getIcp4dToken, + getStreamsAuthToken +}; + +const StreamsRestUtils = { + build, + artifact, + toolkit, + icp4d, + setTimeout +}; + +export default StreamsRestUtils; diff --git a/lib/util/streams-toolkits-utils.js b/lib/util/streams-toolkits-utils.js new file mode 100644 index 0000000..6300ec5 --- /dev/null +++ b/lib/util/streams-toolkits-utils.js @@ -0,0 +1,244 @@ +'use babel'; +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import * as _ from 'lodash'; +import * as semver from 'semver'; +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'); + } + try { + fs.writeFileSync(`${cacheDir}${path.sep}${name}-${version}.xml`, index); + } catch (err) { + throw new Error(`Error caching toolkit index for: ${name}\n${err}`); + } +} + +function getLocalToolkits(pathStr) { + let localToolkits = []; + const localToolkitIndexPaths = getLocalToolkitIndexPaths(pathStr); + localToolkits = localToolkitIndexPaths.map(tkPath => { + try { + const xml = fs.readFileSync(tkPath, 'utf8'); + const document = new xmldoc.XmlDocument(xml); + const tkName = document.childNamed('toolkit').attr.name; + const tkVersion = document.childNamed('toolkit').attr.version; + return { + name: tkName, + version: tkVersion, + indexPath: tkPath, + label: `${tkName} - ${tkVersion}`, + isLocal: true + }; + } catch (err) { + throw new Error(`Error reading toolkit index contents for: ${tkPath}\n${err}`); + } + }); + return localToolkits; +} + +function getCachedToolkits(cachePath) { + let cachedToolkits = []; + if (fs.existsSync(cachePath)) { + const cachedToolkitIndexPaths = getCachedToolkitIndexPaths(cachePath); + cachedToolkits = cachedToolkitIndexPaths.map(tkPath => { + try { + const xml = fs.readFileSync(tkPath, 'utf8'); + const document = new xmldoc.XmlDocument(xml); + const tkName = document.childNamed('toolkit').attr.name; + const tkVersion = document.childNamed('toolkit').attr.version; + return { + name: tkName, + version: tkVersion, + indexPath: tkPath, + label: `${tkName} - ${tkVersion}`, + isLocal: false + }; + } catch (err) { + throw new Error(`Error reading toolkit index contents for: ${tkPath}\n${err}`); + } + }); + } + return cachedToolkits; +} + +function getAllToolkits(cacheDir, tkPathSetting) { + // tkType, tkIndexPath, name, version + return [...getCachedToolkits(cacheDir), ...getLocalToolkits(tkPathSetting)]; +} + +function filterNewestToolkits(toolkitList) { + // returns a uniq'd list where only the newest version of a given toolkit is kept + return _.uniqWith(toolkitList, (tkA, tkB) => tkA.name === tkB.name && semver.gt(tkA.version, tkB.version)); +} + +const StreamsToolkitsUtils = { + refreshLspToolkits, + getLangServerOptionForInitToolkits, + getLangServerParamForAddToolkits, + getLangServerParamForRemoveToolkits, + getLangServerParamForClearToolkits, + getCachedToolkitIndexPaths, + getLocalToolkitIndexPaths, + getChangedLocalToolkits, + cacheToolkitIndex, + getToolkitsToCache, + getLocalToolkits, + getCachedToolkits, + getAllToolkits, + filterNewestToolkits +}; + +export default StreamsToolkitsUtils; diff --git a/lib/util/streams-utils.js b/lib/util/streams-utils.js new file mode 100644 index 0000000..3b3b644 --- /dev/null +++ b/lib/util/streams-utils.js @@ -0,0 +1,90 @@ +'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_MSG_REGEX_V5 = /^(?:\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}\.\d{3}.\s+)([\w.]+(?:\/[\w.]+)?):(\d+):(\d+):\s+(\w{5}\d{4}[IWE])\s+((ERROR|WARN|WARNING|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) { + 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_MSG_REGEX_V5, + 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..09c0da1 --- /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'; +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..8683582 --- /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'; + +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 +