diff --git a/.gitignore b/.gitignore index 0a31f22255..081bfd3b90 100644 --- a/.gitignore +++ b/.gitignore @@ -160,9 +160,6 @@ DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html -# Click-Once directory -publish/ - # Publish Web Output *.[Pp]ublish.xml *.azurePubxml diff --git a/Composer/cypress/integration/LuisDeploy.spec.ts b/Composer/cypress/integration/LuisDeploy.spec.ts index ed904a1a5c..d3048ebb2b 100644 --- a/Composer/cypress/integration/LuisDeploy.spec.ts +++ b/Composer/cypress/integration/LuisDeploy.spec.ts @@ -4,8 +4,9 @@ context('Luis Deploy', () => { beforeEach(() => { cy.server(); - cy.route('POST', '/api/publish/*/publish/default', 'OK'); + cy.route('POST', '/api/publish/*/publish/default', { endpointURL: 'anything' }); cy.route('POST', '/api/projects/*/settings', 'OK'); + cy.route('GET', '/api/publish/*/status/default', { endpointURL: 'anything' }); cy.visit(Cypress.env('COMPOSER_URL')); cy.createBot('ToDoBotWithLuisSample'); }); diff --git a/Composer/cypress/integration/SaveAs.spec.ts b/Composer/cypress/integration/SaveAs.spec.ts index f7f149c4d8..c2cc94c7bf 100644 --- a/Composer/cypress/integration/SaveAs.spec.ts +++ b/Composer/cypress/integration/SaveAs.spec.ts @@ -4,19 +4,16 @@ context('Saving As', () => { beforeEach(() => { cy.visit(Cypress.env('COMPOSER_URL')); - cy.createBot('ToDoBotWithLuisSample'); + cy.createBot('EchoBot', 'TestBot'); }); it('can create a new bot from an existing bot', () => { cy.findByTestId('LeftNav-CommandBarButtonHome').click(); cy.url().should('contain', 'home'); cy.findByText('Save as').click(); - cy.findByTestId('NewDialogName').type('{selectall}__TestSaveAs{enter}'); - cy.findByTestId('ProjectTree').within(() => { cy.findByText('__TestSaveAs.Main').should('exist'); - cy.findByText('View').should('exist'); }); }); }); diff --git a/Composer/packages/client/src/App.tsx b/Composer/packages/client/src/App.tsx index 019164816a..12803c771e 100644 --- a/Composer/packages/client/src/App.tsx +++ b/Composer/packages/client/src/App.tsx @@ -80,6 +80,13 @@ const topLinks = (projectId: string, openedDialogId: string) => { exact: true, disabled: !botLoaded, }, + { + to: `/bot/${projectId}/publish`, + iconName: 'CloudUpload', + labelName: formatMessage('Publish'), + exact: true, + disabled: !botLoaded, + }, { to: `/bot/${projectId}/skills`, iconName: 'PlugDisconnected', diff --git a/Composer/packages/client/src/components/TestController/index.tsx b/Composer/packages/client/src/components/TestController/index.tsx index fcb177e91b..7150cb5e88 100644 --- a/Composer/packages/client/src/components/TestController/index.tsx +++ b/Composer/packages/client/src/components/TestController/index.tsx @@ -7,13 +7,15 @@ import React, { useState, useRef, Fragment, useContext, useEffect, useCallback } import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; import formatMessage from 'format-message'; +import { DefaultPublishConfig } from '../../constants'; + import settingsStorage from './../../utils/dialogSettingStorage'; import { StoreContext } from './../../store'; import { BotStatus, LuisConfig } from './../../constants'; import { isAbsHosted } from './../../utils/envUtil'; import { getReferredFiles } from './../../utils/luUtil'; import useNotifications from './../../pages/notifications/useNotifications'; -import { navigateTo } from './../../utils'; +import { navigateTo, openInEmulator } from './../../utils'; import { PublishLuisDialog } from './publishDialog'; import { bot, botButton } from './styles'; import { ErrorCallout } from './errorCallout'; @@ -21,21 +23,6 @@ import { EmulatorOpenButton } from './emulatorOpenButton'; import { Loading } from './loading'; import { ErrorInfo } from './errorInfo'; -const openInEmulator = (url, authSettings: { MicrosoftAppId: string; MicrosoftAppPassword: string }) => { - // this creates a temporary hidden iframe to fire off the bfemulator protocol - // and start up the emulator - const i = document.createElement('iframe'); - i.style.display = 'none'; - i.onload = () => i.parentNode && i.parentNode.removeChild(i); - i.src = `bfemulator://livechat.open?botUrl=${encodeURIComponent(url)}&msaAppId=${ - authSettings.MicrosoftAppId - }&msaAppPassword=${encodeURIComponent(authSettings.MicrosoftAppPassword)}`; - document.body.appendChild(i); -}; - -const defaultPublishConfig = { - name: 'default', -}; export const TestController: React.FC = () => { const { state, actions } = useContext(StoreContext); const [modalOpen, setModalOpen] = useState(false); @@ -53,7 +40,7 @@ export const TestController: React.FC = () => { useEffect(() => { if (projectId) { - getPublishStatus(projectId, defaultPublishConfig); + getPublishStatus(projectId, DefaultPublishConfig); } }, [projectId]); @@ -95,7 +82,7 @@ export const TestController: React.FC = () => { async function handleLoadBot() { setBotStatus(BotStatus.reloading); const sensitiveSettings = settingsStorage.get(botName); - await publishToTarget(state.projectId, { ...defaultPublishConfig, sensitiveSettings }); + await publishToTarget(state.projectId, DefaultPublishConfig, { comment: '' }, sensitiveSettings); } function isLuisConfigComplete(config) { diff --git a/Composer/packages/client/src/constants/index.ts b/Composer/packages/client/src/constants/index.ts index 628c327c96..9162d5433b 100644 --- a/Composer/packages/client/src/constants/index.ts +++ b/Composer/packages/client/src/constants/index.ts @@ -82,8 +82,9 @@ export enum ActionTypes { PUBLISH_SUCCESS = 'PUBLISH_SUCCESS', PUBLISH_FAILED = 'PUBLISH_FAILED', GET_PUBLISH_STATUS = 'GET_PUBLISH_STATUS', + GET_PUBLISH_STATUS_FAILED = 'GET_PUBLISH_STATUS_FAILED', + GET_PUBLISH_HISTORY = 'GET_PUBLISH_HISTORY', UPDATE_BOTSTATUS = 'UPDATE_BOTSTATUS', - SET_USER_SETTINGS = 'SET_USER_SETTINGS', ADD_SKILL_DIALOG_BEGIN = 'ADD_SKILL_DIALOG_BEGIN', ADD_SKILL_DIALOG_END = 'ADD_SKILL_DIALOG_END', @@ -221,3 +222,8 @@ export const SupportedFileTypes = [ ]; export const USER_TOKEN_STORAGE_KEY = 'composer.userToken'; + +export const DefaultPublishConfig = { + name: 'default', + type: 'localpublish', +}; diff --git a/Composer/packages/client/src/pages/design/styles.ts b/Composer/packages/client/src/pages/design/styles.ts index 41ec6212b6..9d8805f3a5 100644 --- a/Composer/packages/client/src/pages/design/styles.ts +++ b/Composer/packages/client/src/pages/design/styles.ts @@ -23,7 +23,6 @@ export const projectContainer = css` flex-grow: 0; flex-shrink: 0; width: 255px; - height: 100%; overflow: auto; border-right: 1px solid #c4c4c4; `; diff --git a/Composer/packages/client/src/pages/publish/createPublishTarget.tsx b/Composer/packages/client/src/pages/publish/createPublishTarget.tsx new file mode 100644 index 0000000000..1576f5579a --- /dev/null +++ b/Composer/packages/client/src/pages/publish/createPublishTarget.tsx @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import formatMessage from 'format-message'; +import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { Fragment, useState } from 'react'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { JsonEditor } from '@bfc/code-editor'; + +import { label } from './styles'; + +export const CreatePublishTarget = props => { + const [targetType, setTargetType] = useState(props.current ? props.current.type : ''); + const [name, setName] = useState(props.current ? props.current.name : ''); + const [config, setConfig] = useState(props.current ? JSON.parse(props.current.configuration) : {}); + const [errorMessage, setErrorMsg] = useState(''); + + const updateType = (e, type) => { + setTargetType(type.key); + }; + const updateConfig = newConfig => { + setConfig(newConfig); + }; + + const updateName = (e, newName) => { + setErrorMsg(''); + setName(newName); + isNameValid(newName); + }; + + const isNameValid = newName => { + if (!newName || newName.trim() === '') { + setErrorMsg(formatMessage('Must have a name')); + } else { + const exists = + props.targets?.filter(t => { + return t.name.toLowerCase() === newName?.toLowerCase(); + }).length > 0; + if (exists) { + setErrorMsg(formatMessage('A profile with that name already exists.')); + } + } + }; + + const isDisable = () => { + if (!targetType || !name || errorMessage) { + return true; + } else { + return false; + } + }; + + const submit = async () => { + await props.updateSettings(name, targetType, JSON.stringify(config, null, 2) || '{}'); + props.closeDialog(); + }; + + return ( + +
+ + +
{formatMessage('Paste Configuration')}
+ + + + + + +
+ ); +}; diff --git a/Composer/packages/client/src/pages/publish/index.tsx b/Composer/packages/client/src/pages/publish/index.tsx new file mode 100644 index 0000000000..8e8c039de0 --- /dev/null +++ b/Composer/packages/client/src/pages/publish/index.tsx @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { useState, useContext, useEffect, Fragment, useCallback, useMemo } from 'react'; +import { RouteComponentProps } from '@reach/router'; +import formatMessage from 'format-message'; +import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; + +import settingsStorage from '../../utils/dialogSettingStorage'; +import { projectContainer } from '../design/styles'; +import { StoreContext } from '../../store'; +import { navigateTo } from '../../utils'; + +import { TargetList } from './targetList'; +import { PublishDialog } from './publishDialog'; +import { ToolBar } from './../../components/ToolBar/index'; +import { OpenConfirmModal } from './../../components/Modal/Confirm'; +import { ContentHeaderStyle, HeaderText, ContentStyle, contentEditor, overflowSet, targetSelected } from './styles'; +import { CreatePublishTarget } from './createPublishTarget'; +import { PublishStatusList } from './publishStatusList'; + +interface PublishPageProps extends RouteComponentProps<{}> { + targetName?: string; +} + +const Publish: React.FC = props => { + const selectedTargetName = props.targetName; + const [selectedTarget, setSelectedTarget] = useState(); + const { state, actions } = useContext(StoreContext); + const { settings, botName, publishTypes, projectId, publishHistory } = state; + + const [addDialogHidden, setAddDialogHidden] = useState(true); + const [editDialogHidden, setEditDialogHidden] = useState(true); + + const [showLog, setShowLog] = useState(false); + const [publishDialogHidden, setPublishDialogHidden] = useState(true); + + // items to show in the list + const [thisPublishHistory, setThisPublishHistory] = useState([]); + const [groups, setGroups] = useState(); + const [selectedVersion, setSelectedVersion] = useState(); + const [dialogProps, setDialogProps] = useState({ + title: 'Title', + type: DialogType.normal, + children: {}, + }); + const [editDialogProps, setEditDialogProps] = useState({ + title: 'Title', + type: DialogType.normal, + children: {}, + }); + const [editTarget, setEditTarget] = useState(); + + const isRollbackSupported = useMemo( + () => (target, version): boolean => { + if (version.id && version.status === 200 && target) { + const type = publishTypes?.filter(t => t.name === target.type)[0]; + if (type?.features?.rollback) { + return true; + } + } + return false; + }, + [projectId, publishTypes] + ); + + const toolbarItems = [ + { + type: 'action', + text: formatMessage('Add new profile'), + buttonProps: { + iconProps: { + iconName: 'Add', + }, + onClick: () => setAddDialogHidden(false), + }, + align: 'left', + dataTestid: 'publishPage-ToolBar-Add', + disabled: false, + }, + { + type: 'action', + text: formatMessage('Publish to selected profile'), + buttonProps: { + iconProps: { + iconName: 'CloudUpload', + }, + onClick: () => setPublishDialogHidden(false), + }, + align: 'left', + dataTestid: 'publishPage-ToolBar-Publish', + disabled: selectedTargetName !== 'all' ? false : true, + }, + { + type: 'action', + text: formatMessage('See Log'), + buttonProps: { + iconProps: { + iconName: 'ClipboardList', + }, + onClick: () => setShowLog(true), + }, + align: 'left', + disabled: selectedVersion ? false : true, + dataTestid: 'publishPage-ToolBar-Log', + }, + { + type: 'action', + text: formatMessage('Rollback'), + buttonProps: { + iconProps: { + iconName: 'ClipboardList', + }, + onClick: () => rollbackToVersion(selectedVersion), + }, + align: 'left', + disabled: selectedTarget && selectedVersion ? !isRollbackSupported(selectedTarget, selectedVersion) : true, + dataTestid: 'publishPage-ToolBar-Log', + }, + ]; + + const onSelectTarget = useCallback( + targetName => { + const url = `/bot/${projectId}/publish/${targetName}`; + navigateTo(url); + }, + [projectId] + ); + + const getUpdatedStatus = target => { + if (target) { + // TODO: this should use a backoff mechanism to not overload the server with requests + // OR BETTER YET, use a websocket events system to receive updates... (SOON!) + setTimeout(async () => { + await actions.getPublishStatus(projectId, target); + }, 10000); + } + }; + + useEffect(() => { + if (projectId) { + actions.getPublishTargetTypes(); + // init selected status + setSelectedVersion(undefined); + } + }, [projectId]); + + useEffect(() => { + if (settings.publishTargets && settings.publishTargets.length > 0) { + const _selected = settings.publishTargets.find(item => item.name === selectedTargetName); + setSelectedTarget(_selected); + // load publish histories + if (selectedTargetName === 'all') { + for (const target of settings.publishTargets) { + actions.getPublishHistory(projectId, target); + } + } else if (_selected) { + actions.getPublishHistory(projectId, _selected); + } + } + }, [settings.publishTargets, selectedTargetName]); + + // once history is loaded, display it + useEffect(() => { + if (settings.publishTargets && selectedTargetName === 'all') { + let _histories: any[] = []; + const _groups: any[] = []; + let startIndex = 0; + for (const target of settings.publishTargets) { + if (publishHistory[target.name]) { + _histories = _histories.concat(publishHistory[target.name]); + _groups.push({ + key: target.name, + name: target.name, + startIndex: startIndex, + count: publishHistory[target.name].length, + level: 0, + }); + startIndex += publishHistory[target.name].length; + } + } + setGroups(_groups); + setThisPublishHistory(_histories); + } else if (selectedTargetName && publishHistory[selectedTargetName]) { + setThisPublishHistory(publishHistory[selectedTargetName]); + setGroups([ + { + key: selectedTargetName, + name: selectedTargetName, + startIndex: 0, + count: publishHistory[selectedTargetName].length, + level: 0, + }, + ]); + } + }, [publishHistory, selectedTargetName, settings.publishTargets]); + + // check history to see if a 202 is found + useEffect(() => { + // most recent item is a 202, which means we should poll for updates... + if (selectedTargetName !== 'all' && thisPublishHistory.length && thisPublishHistory[0].status === 202) { + getUpdatedStatus(selectedTarget); + } else if (selectedTarget && selectedTarget.lastPublished && thisPublishHistory.length === 0) { + // if the history is EMPTY, but we think we've done a publish based on lastPublished timestamp, + // we still poll for the results IF we see that a publish has happened previously + actions.getPublishStatus(projectId, selectedTarget); + } + }, [thisPublishHistory, selectedTargetName]); + + const savePublishTarget = useMemo( + () => async (name, type, configuration) => { + const _target = (settings.publishTargets || []).concat([ + { + name, + type, + configuration, + }, + ]); + await actions.setSettings( + projectId, + botName, + { + ...settings, + publishTargets: _target, + }, + undefined + ); + onSelectTarget(name); + }, + [settings.publishTargets, projectId, botName] + ); + + const updatePublishTarget = useMemo( + () => async (name, type, configuration) => { + const _targets = settings.publishTargets ? [...settings.publishTargets] : []; + + _targets[editTarget.index] = { + name, + type, + configuration, + }; + + await actions.setSettings( + projectId, + botName, + { + ...settings, + publishTargets: _targets, + }, + undefined + ); + + onSelectTarget(name); + }, + [settings.publishTargets, projectId, botName, editTarget] + ); + + useEffect(() => { + setDialogProps({ + title: formatMessage('Add a publish profile'), + type: DialogType.normal, + children: ( + { + return { key: type.name, text: type.name }; + })} + targets={settings.publishTargets} + updateSettings={savePublishTarget} + current={null} + closeDialog={() => setAddDialogHidden(true)} + /> + ), + }); + }, [publishTypes, savePublishTarget, settings.publishTargets]); + + useEffect(() => { + setEditDialogProps({ + title: formatMessage('Edit a publish profile'), + type: DialogType.normal, + children: ( + { + return { key: type.name, text: type.name }; + })} + current={editTarget ? editTarget.item : null} + targets={settings.publishTargets?.filter(item => editTarget && item.name != editTarget.item.name)} + updateSettings={updatePublishTarget} + closeDialog={() => setEditDialogHidden(true)} + /> + ), + }); + }, [editTarget, publishTypes, updatePublishTarget]); + + const rollbackToVersion = useMemo( + () => async version => { + const sensitiveSettings = settingsStorage.get(botName); + await actions.rollbackToVersion(projectId, selectedTarget, version.id, sensitiveSettings); + }, + [projectId, selectedTarget] + ); + + const publish = useMemo( + () => async comment => { + // publish to remote + if (selectedTarget && settings.publishTargets) { + const sensitiveSettings = settingsStorage.get(botName); + await actions.publishToTarget(projectId, selectedTarget, { comment: comment }, sensitiveSettings); + + // update the target with a lastPublished date + const updatedPublishTargets = settings.publishTargets.map(profile => { + if (profile.name === selectedTarget.name) { + return { + ...profile, + lastPublished: new Date(), + }; + } else { + return profile; + } + }); + + await actions.setSettings( + projectId, + botName, + { + ...settings, + publishTargets: updatedPublishTargets, + }, + undefined + ); + } + }, + [projectId, selectedTarget, settings.publishTargets] + ); + + const onEdit = async (index: number, item: any) => { + const newItem = { item: item, index: index }; + setEditTarget(newItem); + setEditDialogHidden(false); + }; + + const onDelete = useMemo( + () => async (index: number) => { + const result = await OpenConfirmModal( + formatMessage('This will delete the profile. Do you wish to continue?'), + null, + { + confirmBtnText: formatMessage('Yes'), + cancelBtnText: formatMessage('Cancel'), + } + ); + + if (result) { + if (settings.publishTargets && settings.publishTargets.length > index) { + const _target = settings.publishTargets.slice(0, index).concat(settings.publishTargets.slice(index + 1)); + await actions.setSettings( + projectId, + botName, + { + ...settings, + publishTargets: _target, + }, + undefined + ); + // redirect to all profiles + setSelectedTarget(undefined); + onSelectTarget('all'); + } + } + }, + [settings.publishTargets, projectId, botName] + ); + + return ( + + + + {!publishDialogHidden && ( + setPublishDialogHidden(true)} onSubmit={publish} target={selectedTarget} /> + )} + {showLog && setShowLog(false)} />} + +
+

{selectedTarget ? selectedTargetName : formatMessage('Publish Profiles')}

+
+
+
+
{ + setSelectedTarget(undefined); + onSelectTarget('all'); + }} + css={selectedTargetName === 'all' ? targetSelected : overflowSet} + style={{ + height: '36px', + cursor: 'pointer', + }} + > + {formatMessage('All profiles')} +
+ {settings && settings.publishTargets && ( + { + setSelectedTarget(item); + onSelectTarget(item.name); + }} + onEdit={async (item, target) => await onEdit(item, target)} + onDelete={async index => await onDelete(index)} + selectedTarget={selectedTargetName} + /> + )} +
+
+ + + {!thisPublishHistory || thisPublishHistory.length === 0 ? ( +
No publish history
+ ) : null} +
+
+
+
+ ); +}; + +export default Publish; +const LogDialog = props => { + const logDialogProps = { + title: 'Publish Log', + }; + return ( + + ); +}; diff --git a/Composer/packages/client/src/pages/publish/publishDialog.tsx b/Composer/packages/client/src/pages/publish/publishDialog.tsx new file mode 100644 index 0000000000..6aa7f989e5 --- /dev/null +++ b/Composer/packages/client/src/pages/publish/publishDialog.tsx @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button'; +import { useState, Fragment } from 'react'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import formatMessage from 'format-message'; + +import { publishDialogText } from './styles'; + +export const PublishDialog = props => { + const [comment, setComment] = useState(''); + const publishDialogProps = { + title: 'Publish', + type: DialogType.normal, + subText: 'You are about to publish your bot to the profile below. Do you want to proceed?', + }; + const submit = async () => { + props.onDismiss(); + await props.onSubmit(comment); + }; + return props.target ? ( + + ) : null; +}; diff --git a/Composer/packages/client/src/pages/publish/publishStatusList.tsx b/Composer/packages/client/src/pages/publish/publishStatusList.tsx new file mode 100644 index 0000000000..35622aac81 --- /dev/null +++ b/Composer/packages/client/src/pages/publish/publishStatusList.tsx @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { + DetailsList, + DetailsListLayoutMode, + SelectionMode, + IColumn, + IGroup, + CheckboxVisibility, +} from 'office-ui-fabric-react/lib/DetailsList'; +import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; +import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; +import { Selection } from 'office-ui-fabric-react/lib/DetailsList'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; +import moment from 'moment'; +import { useMemo, useState, useEffect } from 'react'; + +import { listRoot, tableView, detailList } from './styles'; + +export interface IStatusListProps { + items: IStatus[]; + groups: IGroup[]; + onItemClick: (item: IStatus | null) => void; + updateItems: (items: IStatus[]) => void; +} + +export interface IStatus { + id: string; + time: string; + status: number; + message: string; + comment: string; +} + +function onRenderDetailsHeader(props, defaultRender) { + return ( + + {defaultRender({ + ...props, + onRenderColumnHeaderTooltip: tooltipHostProps => , + })} + + ); +} + +export const PublishStatusList: React.FC = props => { + const { items, onItemClick, groups } = props; + const [selectIndex, setSelectedIndex] = useState(); + const [currentSort, setSort] = useState({ key: 'PublishDate', descending: true }); + const sortByDate = (ev: React.MouseEvent, column: IColumn): void => { + if (column.isSorted) { + column.isSortedDescending = !column.isSortedDescending; + let newItems: IStatus[] = []; + for (const group of groups) { + newItems = newItems.concat(items.slice(group.startIndex, group.startIndex + group.count).reverse()); + } + props.updateItems(newItems); + } + }; + const columns = [ + { + key: 'PublishTime', + name: 'Time', + className: 'publishtime', + fieldName: 'time', + minWidth: 70, + maxWidth: 90, + isRowHeader: true, + isResizable: true, + data: 'string', + onRender: (item: IStatus) => { + return {moment(item.time).format('h:mm a')}; + }, + isPadded: true, + }, + { + key: 'PublishDate', + name: 'Date', + className: 'publishdate', + fieldName: 'date', + minWidth: 70, + maxWidth: 90, + isRowHeader: true, + isResizable: true, + onColumnClick: sortByDate, + data: 'string', + onRender: (item: IStatus) => { + return {moment(item.time).format('DD-MM-YYYY')}; + }, + isPadded: true, + }, + { + key: 'PublishStatus', + name: 'Status', + className: 'publishstatus', + fieldName: 'status', + minWidth: 70, + maxWidth: 90, + isResizable: true, + data: 'string', + onRender: (item: IStatus) => { + if (item.status === 200) { + return ; + } else if (item.status === 202) { + return ( +
+ +
+ ); + } else { + return ; + } + }, + isPadded: true, + }, + { + key: 'PublishMessage', + name: 'Message', + className: 'publishmessage', + fieldName: 'message', + minWidth: 70, + maxWidth: 90, + isResizable: true, + isCollapsible: true, + isMultiline: true, + data: 'string', + onRender: (item: IStatus) => { + return {item.message}; + }, + isPadded: true, + }, + { + key: 'PublishComment', + name: 'Comment', + className: 'comment', + fieldName: 'comment', + minWidth: 70, + maxWidth: 90, + isResizable: true, + isCollapsible: true, + isMultiline: true, + data: 'string', + onRender: (item: IStatus) => { + return {item.comment}; + }, + isPadded: true, + }, + ]; + const selection = useMemo(() => { + return new Selection({ + onSelectionChanged: () => { + const selectedIndexs = selection.getSelectedIndices(); + if (selectedIndexs.length > 0) { + setSelectedIndex(selectedIndexs[0]); + } + }, + }); + }, [items, groups]); + + useEffect(() => { + // init the selected publish status after switch to another target + setSelectedIndex(null); + }, [groups]); + + useEffect(() => { + if (items && typeof selectIndex === 'number' && items.length > selectIndex) { + onItemClick(items[selectIndex]); + } else { + onItemClick(null); + } + }, [selectIndex, items]); + + return ( +
+
+ ({ + ...col, + isSorted: col.key === currentSort.key, + isSortedDescending: currentSort.descending, + }))} + groups={groups} + selection={selection} + selectionMode={SelectionMode.single} + getKey={item => item.id} + setKey="none" + onColumnHeaderClick={(_, clickedCol) => { + if (!clickedCol) return; + if (clickedCol.key === currentSort.key) { + clickedCol.isSortedDescending = !currentSort.descending; + setSort({ key: clickedCol.key, descending: !currentSort.descending }); + } else { + clickedCol.isSorted = false; + } + }} + layoutMode={DetailsListLayoutMode.justified} + isHeaderVisible={true} + checkboxVisibility={CheckboxVisibility.hidden} + onRenderDetailsHeader={onRenderDetailsHeader} + groupProps={{ + showEmptyGroups: true, + }} + /> +
+
+ ); +}; diff --git a/Composer/packages/client/src/pages/publish/styles.ts b/Composer/packages/client/src/pages/publish/styles.ts new file mode 100644 index 0000000000..f4c372c28c --- /dev/null +++ b/Composer/packages/client/src/pages/publish/styles.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { css } from '@emotion/core'; +import { FontWeights, FontSizes } from 'office-ui-fabric-react/lib/Styling'; +import { NeutralColors } from '@uifabric/fluent-theme'; + +export const ContentHeaderStyle = css` + padding: 5px 20px; + height: 60px; + display: flex; + flex-shrink: 0; + justify-content: space-between; + align-items: center; +`; +export const HeaderText = css` + font-size: ${FontSizes.xLarge}; + font-weight: ${FontWeights.semibold}; + margin-right: 10px; +`; + +export const ContentStyle = css` + margin-left: 2px; + display: flex; + border-top: 1px solid #dddddd; + flex: 1; + position: relative; + nav { + ul { + margin-top: 0px; + } + } +`; +export const contentEditor = css` + flex: 4; + height: calc(100vh - 200px); + position: relative; + overflow: visible; +`; + +export const publishDialogText = css` + background-color: #ddf3db; + margin-bottom: 10px; + font-size: medium; + padding: 7px; +`; + +export const historyPanelTitle = css` + font-size: ${FontSizes.xLarge}; + font-weight: 600; + margin-right: 10px; +`; + +export const historyPanelSub = css` + font-size: ${FontSizes.small}; +`; + +export const targetListTiTle = css` + height: 32px; + font-size: ${FontSizes.medium}; + padding-left: 16px; + padding-top: 6px; + padding-right: 0; + font-weight: 600; +`; + +export const listRoot = css` + position: relative; + overflow-y: auto; + flex-grow: 1; + display: flex; + flex-direction: column; +`; + +export const tableView = css` + position: relative; + flex-grow: 1; +`; + +export const detailList = css` + overflow-x: hidden; +`; + +export const label = css` + font-size: 14px; + font-weight: 600; + color: #323130; + padding: 5px 0px; +`; + +export const overflowSet = css` + width: 100%; + height: 100%; + box-sizing: border-box; + justify-content: space-between; + line-height: 36px; + padding-left: 16px; + background: ${NeutralColors.white}; + font-weight: ${FontWeights.semibold}; + font-size: ${FontSizes.small}; + &:hover { + background: ${NeutralColors.gray20}; + font-weight: ${FontWeights.bold}; + } +`; + +export const targetSelected = css` + width: 100%; + height: 100%; + box-sizing: border-box; + justify-content: space-between; + line-height: 36px; + padding-left: 16px; + background: ${NeutralColors.gray20}; + font-weight: ${FontWeights.bold}; + font-size: ${FontSizes.small}; +`; diff --git a/Composer/packages/client/src/pages/publish/targetList.tsx b/Composer/packages/client/src/pages/publish/targetList.tsx new file mode 100644 index 0000000000..3071732766 --- /dev/null +++ b/Composer/packages/client/src/pages/publish/targetList.tsx @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Fragment } from 'react'; +import { OverflowSet, IOverflowSetItemProps } from 'office-ui-fabric-react/lib/OverflowSet'; + +import { overflowSet, targetSelected } from './styles'; + +export const TargetList = props => { + const onRenderOverflowButton = (overflowItems: any[] | undefined) => { + return ( + + ); + }; + const onRenderItem = (item: IOverflowSetItemProps) => { + const { name, key } = item; + return
{name}
; + }; + + return ( + + {props.list.map((target, index) => { + return ( +
props.onSelect(target)}> + props.onDelete(index, target), + }, + { + key: 'edit', + name: 'Edit', + onClick: () => props.onEdit(index, target), + }, + ]} + onRenderItem={onRenderItem} + onRenderOverflowButton={onRenderOverflowButton} + /> +
+ ); + })} +
+ ); +}; diff --git a/Composer/packages/client/src/pages/setting/publisher/index.js b/Composer/packages/client/src/pages/setting/publisher/index.js deleted file mode 100644 index b4f9d4e63b..0000000000 --- a/Composer/packages/client/src/pages/setting/publisher/index.js +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useContext, useEffect, useState, Fragment } from 'react'; -import formatMessage from 'format-message'; -import TimeAgo from 'react-timeago'; -import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; -import { Icon } from 'office-ui-fabric-react/lib/Icon'; -import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; -import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog'; -import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; -import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; - -import { isAbsHosted } from '../../../utils/envUtil'; - -import { StoreContext } from './../../../store'; -import { styles } from './styles'; - -const DateWidget = props => { - const { date } = props; - - const timestamp = new Date(date); - const now = new Date(); - - const minutesAgo = parseInt((now.getTime() - timestamp.getTime()) / 60000); - - if (minutesAgo < 60) { - return ; - } else { - const formattedDate = timestamp.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' }); - return {formattedDate}; - } -}; - -export const Publisher = () => { - const { state, actions } = useContext(StoreContext); - const [dialogHidden, setDialogHidden] = useState(true); - const [publishAction, setPublishAction] = useState(''); - const [dialogProps, setDialogProps] = useState({ - title: 'Title', - subText: 'Sub Text', - type: DialogType.normal, - children: [], - }); - const { - botName, - botEnvironment, - publishVersions, - remoteEndpoints, - settings, - publishStatus, - publishTypes, - publishTargets, - } = state; - const [slot, setSlot] = useState(botEnvironment === 'editing' ? 'integration' : botEnvironment); - const absHosted = isAbsHosted(); - - useEffect(() => { - // load up the list of all publish targets - actions.getPublishTargetTypes(); - - // display current targets - updatePublishTargets(settings.publishTargets || []); - }, []); - - const updatePublishTargets = async rawTargets => { - // make sure there is space for the status to be loaded - const targets = rawTargets.map(target => { - return { - status: '', - statusCode: 202, - ...target, - }; - }); - - console.log('SET PUBLISH TARGETS', targets); - for (let i = 0; i < targets.length; i++) { - publishTargets.push(targets[i]); - } - }; - - useEffect(() => { - console.log('publish targets changed'); - if ( - publishTargets.filter(target => { - return target.statusCode === 202; - }).length - ) { - actions.getPublishStatus(); - console.log('NEED TO LOAD PUBLISH STATUS'); - } - }, [publishTargets]); - - const savePublishTarget = (name, type, configuration) => { - alert(`save ${name} ${type} ${configuration}`); - - if (!settings.publishTargets) { - settings.publishTargets = []; - } - - settings.publishTargets.push({ - name, - type, - configuration, - }); - - actions.setSettings(state.projectId, botName, settings, absHosted ? slot : undefined); - - updatePublishTargets(settings.publishTargets || []); - }; - - const addDestination = async () => { - setDialogProps({ - title: 'Add Target', - type: DialogType.normal, - children: ( - { - return { key: type, text: type }; - })} - onSave={savePublishTarget} - /> - ), - }); - setDialogHidden(false); - }; - - const closeConfirm = () => { - setDialogHidden(true); - }; - - const publishToTarget = index => { - return async () => { - if (publishTargets[index]) { - const target = publishTargets[index]; - console.log('PUBLISH TO TARGET', target); - await actions.publishToTarget(state.projectId, target); - } - }; - }; - - return ( -
-

Publish your bot to a remote

- - {publishTypes && publishTypes.length &&
publish to types: {publishTypes.join(',')}
} - - {publishTargets.map((target, i) => { - return ( -
-

- - {target.name} - -

-
- ); - })} - - {!publishVersions &&
{formatMessage('Loading')}
} - - -
- ); -}; - -const CreatePublishTarget = props => { - let targetType = ''; - let config = ''; - let name = ''; - const updateType = (e, type) => { - // console.log('UPDATE TYPE', type); - targetType = type.key; - }; - const updateConfig = (e, newConfig) => { - // console.log('UPDATE CONFIG', config); - // todo: attempt json parse and only allow submit if json is valid - config = newConfig; - }; - const updateName = (e, newName) => { - name = newName; - }; - - const submit = () => { - try { - JSON.parse(config); - return props.onSave(name, targetType, config); - } catch (err) { - alert('Error parsing configuration'); - } - }; - - return ( - -
- create a publish target. - - - - - - - -
- ); -}; diff --git a/Composer/packages/client/src/pages/setting/publisher/styles.js b/Composer/packages/client/src/pages/setting/publisher/styles.js deleted file mode 100644 index dc264209c1..0000000000 --- a/Composer/packages/client/src/pages/setting/publisher/styles.js +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -module.exports.styles = { - published: { - backgroundColor: '#F2F2F2', - padding: '1rem', - width: '400px', - marginBottom: '1rem', - fontSize: '0.6875rem', - }, - integration: { - padding: '1rem', - backgroundColor: 'rgba(0, 120, 212, 0.1)', - width: '400px', - marginBottom: '1rem', - fontSize: '0.6875rem', - }, - rollback: { - padding: '1rem', - paddingTop: '0', - width: '400px', - fontSize: '0.6875rem', - }, - h1: { - margin: '0px', - fontSize: '0.81rem', - }, - buttons: { - root: { - width: '150px', - }, - }, - button: { - root: [ - { - width: '100%', - fontSize: '14px', - padding: '0px', - }, - ], - }, - icon: { - root: [ - { - width: '22px', - height: '22px', - fontSize: '22px', - }, - ], - }, - page: { - padding: '1rem', - }, - header: { - marginTop: '0', - }, - disabled: { - root: [ - { - color: '#808080', - }, - ], - }, - disabledIcon: { - root: [ - { - width: '22px', - height: '22px', - fontSize: '22px', - color: '#808080', - }, - ], - }, -}; diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index 5764f05ce5..8e90cea9af 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -20,6 +20,7 @@ const LUPage = React.lazy(() => import('./pages/language-understanding')); const LGPage = React.lazy(() => import('./pages/language-generation')); const SettingPage = React.lazy(() => import('./pages/setting')); const Notifications = React.lazy(() => import('./pages/notifications')); +const Publish = React.lazy(() => import('./pages/publish')); const Skills = React.lazy(() => import('./pages/skills')); const Routes = props => { @@ -39,12 +40,14 @@ const Routes = props => { to="/bot/:projectId/language-understanding/all" noThrow /> + + diff --git a/Composer/packages/client/src/store/action/publisher.ts b/Composer/packages/client/src/store/action/publisher.ts index 80186af139..5c65322759 100644 --- a/Composer/packages/client/src/store/action/publisher.ts +++ b/Composer/packages/client/src/store/action/publisher.ts @@ -12,26 +12,60 @@ export const getPublishTargetTypes: ActionCreator = async ({ dispatch }) => { dispatch({ type: ActionTypes.GET_PUBLISH_TYPES_SUCCESS, payload: { - response: response.data, + typelist: response.data, }, }); } catch (err) { - // dispatch({ type: ActionTypes.GET_PUBLISH_TYPES_FAILURE, payload: null, error: err }); + dispatch({ + type: ActionTypes.SET_ERROR, + payload: err, + }); } }; -export const publishToTarget: ActionCreator = async ({ dispatch }, projectId, target) => { +export const publishToTarget: ActionCreator = async ({ dispatch }, projectId, target, metadata, sensitiveSettings) => { try { - const response = await httpClient.post(`/publish/${projectId}/publish/${target.name}`, target.sensitiveSettings); + const response = await httpClient.post(`/publish/${projectId}/publish/${target.name}`, { + metadata, + sensitiveSettings, + }); dispatch({ type: ActionTypes.PUBLISH_SUCCESS, - payload: response.data, + payload: { + ...response.data, + target: target, + }, }); } catch (err) { dispatch({ type: ActionTypes.PUBLISH_FAILED, payload: { error: err.response.data, + target: target, + }, + }); + } +}; + +export const rollbackToVersion: ActionCreator = async ({ dispatch }, projectId, target, version, sensitiveSettings) => { + try { + const response = await httpClient.post(`/publish/${projectId}/rollback/${target.name}`, { + version, + sensitiveSettings, + }); + dispatch({ + type: ActionTypes.PUBLISH_SUCCESS, + payload: { + ...response.data, + target: target, + }, + }); + } catch (err) { + dispatch({ + type: ActionTypes.PUBLISH_FAILED, + payload: { + error: err.response.data, + target: target, }, }); } @@ -43,9 +77,36 @@ export const getPublishStatus: ActionCreator = async ({ dispatch }, projectId, t const response = await httpClient.get(`/publish/${projectId}/status/${target.name}`); dispatch({ type: ActionTypes.GET_PUBLISH_STATUS, - payload: response.data, + payload: { + ...response.data, + target: target, + }, }); } catch (err) { - console.error(err.response.data.message); + dispatch({ + type: ActionTypes.GET_PUBLISH_STATUS_FAILED, + payload: { + ...err.response.data, + target: target, + }, + }); + } +}; + +export const getPublishHistory: ActionCreator = async ({ dispatch }, projectId, target) => { + try { + const response = await httpClient.get(`/publish/${projectId}/history/${target.name}`); + dispatch({ + type: ActionTypes.GET_PUBLISH_HISTORY, + payload: { + history: response.data, + target: target, + }, + }); + } catch (err) { + dispatch({ + type: ActionTypes.SET_ERROR, + payload: err, + }); } }; diff --git a/Composer/packages/client/src/store/index.tsx b/Composer/packages/client/src/store/index.tsx index 639994e4d3..8964042312 100644 --- a/Composer/packages/client/src/store/index.tsx +++ b/Composer/packages/client/src/store/index.tsx @@ -78,7 +78,7 @@ const initialState: State = { }, clipboardActions: [], publishTypes: [], - publishTargets: [], + publishHistory: {}, userSettings: storage.get('userSettings', { codeEditor: { lineNumbers: false, diff --git a/Composer/packages/client/src/store/reducer/index.ts b/Composer/packages/client/src/store/reducer/index.ts index e09b05368b..bfcfe8b273 100644 --- a/Composer/packages/client/src/store/reducer/index.ts +++ b/Composer/packages/client/src/store/reducer/index.ts @@ -417,27 +417,68 @@ const setUserSessionExpired: ReducerFunc = (state, { expired } = {}) => { return state; }; -const setPublishTypes: ReducerFunc = (state, { response }) => { - state.publishTypes = response; +const setPublishTypes: ReducerFunc = (state, { typelist }) => { + state.publishTypes = typelist; return state; }; const publishSuccess: ReducerFunc = (state, payload) => { - state.botEndpoints[state.projectId] = `${payload.results?.result?.endpoint || 'http://localhost:3979'}/api/messages`; - state.botStatus = BotStatus.connected; + if (payload.target.name === 'default' && payload.endpointURL) { + state.botEndpoints[state.projectId] = `${payload.endpointURL}/api/messages`; + state.botStatus = BotStatus.connected; + } + + // prepend the latest publish results to the history + if (!state.publishHistory[payload.target.name]) { + state.publishHistory[payload.target.name] = []; + } + state.publishHistory[payload.target.name].unshift(payload); + return state; }; -const publishFailure: ReducerFunc = (state, { error }) => { - state.botStatus = BotStatus.failed; - state.botLoadErrorMsg = { title: Text.CONNECTBOTFAILURE, message: error.message }; +const publishFailure: ReducerFunc = (state, { error, target }) => { + if (target.name === 'default') { + state.botStatus = BotStatus.failed; + state.botLoadErrorMsg = { title: Text.CONNECTBOTFAILURE, message: error.message }; + } + // prepend the latest publish results to the history + if (!state.publishHistory[target.name]) { + state.publishHistory[target.name] = []; + } + state.publishHistory[target.name].unshift(error); return state; }; const getPublishStatus: ReducerFunc = (state, payload) => { - if (payload.results?.botStatus === 'connected') { + // the action below only applies to when a bot is being started using the "start bot" button + // a check should be added to this that ensures this ONLY applies to the "default" profile. + if (payload.target.name === 'default' && payload.endpointURL) { state.botStatus = BotStatus.connected; + state.botEndpoints[state.projectId] = `${payload.endpointURL}/api/messages`; } + + // if no history exists, create one with the latest status + // otherwise, replace the latest publish history with this one + if (!state.publishHistory[payload.target.name] && payload.status !== 404) { + state.publishHistory[payload.target.name] = [payload]; + } else if (payload.status !== 404) { + // make sure this status payload represents the same item as item 0 (most of the time) + // otherwise, prepend it to the list to indicate a NEW publish has occurred since last loading history + if ( + state.publishHistory[payload.target.name].length && + state.publishHistory[payload.target.name][0].id === payload.id + ) { + state.publishHistory[payload.target.name][0] = payload; + } else { + state.publishHistory[payload.target.name].unshift(payload); + } + } + return state; +}; + +const getPublishHistory: ReducerFunc = (state, payload) => { + state.publishHistory[payload.target.name] = payload.history; return state; }; @@ -524,6 +565,8 @@ export const reducer = createReducer({ [ActionTypes.PUBLISH_SUCCESS]: publishSuccess, [ActionTypes.PUBLISH_FAILED]: publishFailure, [ActionTypes.GET_PUBLISH_STATUS]: getPublishStatus, + [ActionTypes.GET_PUBLISH_STATUS_FAILED]: getPublishStatus, + [ActionTypes.GET_PUBLISH_HISTORY]: getPublishHistory, [ActionTypes.REMOVE_RECENT_PROJECT]: removeRecentProject, [ActionTypes.EDITOR_SELECTION_VISUAL]: setVisualEditorSelection, [ActionTypes.ONBOARDING_ADD_COACH_MARK_REF]: onboardingAddCoachMarkRef, diff --git a/Composer/packages/client/src/store/types.ts b/Composer/packages/client/src/store/types.ts index fc92f92ea6..fcf683ea01 100644 --- a/Composer/packages/client/src/store/types.ts +++ b/Composer/packages/client/src/store/types.ts @@ -46,6 +46,23 @@ export interface StorageFolder extends File { writable?: boolean; } +export interface PublishType { + name: string; + description: string; + features: { + history: boolean; + publish: boolean; + rollback: boolean; + status: boolean; + }; +} + +export interface PublishTarget { + name: string; + type: PublishType; + configuration: any; +} + export interface State { dialogs: DialogInfo[]; projectId: string; @@ -96,8 +113,10 @@ export interface State { complete: boolean; }; clipboardActions: any[]; - publishTypes: string[]; - publishTargets: any[]; + publishTypes: PublishType[]; + publishHistory: { + [key: string]: any[]; + }; userSettings: UserSettings; announcement: string | undefined; } @@ -122,6 +141,7 @@ export interface DialogSetting { MicrosoftAppId?: string; MicrosoftAppPassword?: string; luis?: ILuisConfig; + publishTargets?: PublishTarget[]; [key: string]: any; } diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index 0868be45fe..0007b456a7 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -128,3 +128,15 @@ export function navigateTo(to: string, navigateOpts: NavigateOptions { + // this creates a temporary hidden iframe to fire off the bfemulator protocol + // and start up the emulator + const i = document.createElement('iframe'); + i.style.display = 'none'; + i.onload = () => i.parentNode && i.parentNode.removeChild(i); + i.src = `bfemulator://livechat.open?botUrl=${encodeURIComponent(url)}&msaAppId=${ + authSettings.MicrosoftAppId + }&msaAppPassword=${encodeURIComponent(authSettings.MicrosoftAppPassword)}`; + document.body.appendChild(i); +}; diff --git a/Composer/packages/server/src/controllers/publisher.ts b/Composer/packages/server/src/controllers/publisher.ts index 5c674bffb3..c723ef560c 100644 --- a/Composer/packages/server/src/controllers/publisher.ts +++ b/Composer/packages/server/src/controllers/publisher.ts @@ -8,45 +8,76 @@ import merge from 'lodash/merge'; import { pluginLoader, PluginLoader } from '../services/pluginLoader'; import { BotProjectService } from '../services/project'; import { runtimeFolder } from '../settings/env'; + const defaultPublishConfig = { name: 'default', type: 'localpublish', + configuration: JSON.stringify({}), }; const DEFAULT_RUNTIME = 'CSharp'; export const PublishController = { getTypes: async (req, res) => { - res.json(Object.keys(pluginLoader.extensions.publish)); + res.json( + Object.keys(pluginLoader.extensions.publish) + .filter(i => pluginLoader.extensions.publish[i].plugin.name !== defaultPublishConfig.type) + .map(i => { + return { + name: pluginLoader.extensions.publish[i].plugin.name, + description: pluginLoader.extensions.publish[i].plugin.description, + features: { + history: pluginLoader.extensions.publish[i].methods.history ? true : false, + publish: pluginLoader.extensions.publish[i].methods.publish ? true : false, + status: pluginLoader.extensions.publish[i].methods.getStatus ? true : false, + rollback: pluginLoader.extensions.publish[i].methods.rollback ? true : false, + }, + }; + }) + ); }, publish: async (req, res) => { const target = req.params.target; const user = await PluginLoader.getUserFromRequest(req); - const sensitiveSetting = req.body; + const { metadata, sensitiveSettings } = req.body; const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); - // find publish config by name. - const configs = currentProject.settings?.publishTargets?.filter(t => t.name === target) || [defaultPublishConfig]; - const config = configs.length ? configs[0] : undefined; - const method = config ? config.type : undefined; + // deal with publishTargets not exist in settings + const publishTargets = currentProject.settings?.publishTargets || []; + const allTargets = [defaultPublishConfig, ...publishTargets]; + + const profiles = allTargets.filter(t => t.name === target); + const profile = profiles.length ? profiles[0] : undefined; + const method = profile ? profile.type : undefined; // append config from client(like sensitive settings) - config.configuration = { - ...config.configuration, - settings: merge({}, currentProject.settings, sensitiveSetting), + const configuration = { + name: profile.name, + ...JSON.parse(profile.configuration), + settings: merge({}, currentProject.settings, sensitiveSettings), templatePath: path.resolve(runtimeFolder, DEFAULT_RUNTIME), }; - if (config && pluginLoader.extensions.publish[method] && pluginLoader.extensions.publish[method].publish) { + if ( + profile && + pluginLoader.extensions.publish[method] && + pluginLoader.extensions.publish[method].methods && + pluginLoader.extensions.publish[method].methods.publish + ) { // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].publish; + const pluginMethod = pluginLoader.extensions.publish[method].methods.publish; try { // call the method - const results = await pluginMethod.call(null, config.configuration, currentProject, user); - res.json({ - target: target, - results: results, - }); + const results = await pluginMethod.call(null, configuration, currentProject, metadata, user); + + // copy status into payload for ease of access in client + const response = { + ...results.result, + status: results.status, + }; + + // set status and return value as json + res.status(results.status).json(response); } catch (err) { res.status(400).json({ statusCode: '400', @@ -66,31 +97,143 @@ export const PublishController = { const projectId = req.params.projectId; const currentProject = await BotProjectService.getProjectById(projectId, user); - // find publish config by name. - const configs = currentProject.settings?.publishTargets?.filter(t => t.name === target) || [defaultPublishConfig]; - const config = configs.length ? configs[0] : undefined; - const method = config ? config.type : undefined; - if (pluginLoader.extensions.publish[method] && pluginLoader.extensions.publish[method].getStatus) { + const publishTargets = currentProject.settings?.publishTargets || []; + const allTargets = [defaultPublishConfig, ...publishTargets]; + + const profiles = allTargets.filter(t => t.name === target); + const profile = profiles.length ? profiles[0] : undefined; + + const method = profile ? profile.type : undefined; + if ( + profile && + pluginLoader.extensions.publish[method] && + pluginLoader.extensions.publish[method].methods && + pluginLoader.extensions.publish[method].methods.getStatus + ) { // get the externally defined method - const pluginMethod = pluginLoader.extensions.publish[method].getStatus; + const pluginMethod = pluginLoader.extensions.publish[method].methods.getStatus; - // call the method - const results = await pluginMethod.call(null, projectId, {}); - res.json({ - target: target, - results: results, - }); - } else { - res.status(400).json({ - statusCode: '400', - message: `${method} is not a valid publishing target type. There may be a missing plugin.`, - }); + if (typeof pluginMethod === 'function') { + const configuration = { + name: profile.name, + ...JSON.parse(profile.configuration), + }; + + // call the method + const results = await pluginMethod.call(null, configuration, currentProject, user); + // copy status into payload for ease of access in client + const response = { + ...results.result, + status: results.status, + }; + + // set status and return value as json + return res.status(results.status).json(response); + } } + + res.status(400).json({ + statusCode: '400', + message: `${method} is not a valid publishing target type. There may be a missing plugin.`, + }); }, history: async (req, res) => { - // TODO + const target = req.params.target; + const user = await PluginLoader.getUserFromRequest(req); + const projectId = req.params.projectId; + const currentProject = await BotProjectService.getProjectById(projectId, user); + + const publishTargets = currentProject.settings?.publishTargets || []; + const allTargets = [defaultPublishConfig, ...publishTargets]; + + const profiles = allTargets.filter(t => t.name === target); + const profile = profiles.length ? profiles[0] : undefined; + + const method = profile ? profile.type : undefined; + + if ( + profile && + pluginLoader.extensions.publish[method] && + pluginLoader.extensions.publish[method].methods && + pluginLoader.extensions.publish[method].methods.history + ) { + // get the externally defined method + const pluginMethod = pluginLoader.extensions.publish[method].methods.history; + if (typeof pluginMethod === 'function') { + const configuration = { + name: profile.name, + ...JSON.parse(profile.configuration), + }; + + // call the method + const results = await pluginMethod.call(null, configuration, currentProject, user); + + // set status and return value as json + return res.status(200).json(results); + } + } + + res.status(400).json({ + statusCode: '400', + message: `${method} is not a valid publishing target type. There may be a missing plugin.`, + }); }, rollback: async (req, res) => { - // TODO + const target = req.params.target; + const user = await PluginLoader.getUserFromRequest(req); + const { version, sensitiveSettings } = req.body; + const projectId = req.params.projectId; + const currentProject = await BotProjectService.getProjectById(projectId, user); + + // deal with publishTargets not exist in settings + const publishTargets = currentProject.settings?.publishTargets || []; + const allTargets = [defaultPublishConfig, ...publishTargets]; + + const profiles = allTargets.filter(t => t.name === target); + const profile = profiles.length ? profiles[0] : undefined; + const method = profile ? profile.type : undefined; + + // append config from client(like sensitive settings) + const configuration = { + name: profile.name, + ...JSON.parse(profile.configuration), + settings: merge({}, currentProject.settings, sensitiveSettings), + templatePath: path.resolve(runtimeFolder, DEFAULT_RUNTIME), + }; + + if ( + profile && + pluginLoader.extensions.publish[method] && + pluginLoader.extensions.publish[method].methods && + pluginLoader.extensions.publish[method].methods.rollback + ) { + // get the externally defined method + const pluginMethod = pluginLoader.extensions.publish[method].methods.rollback; + if (typeof pluginMethod === 'function') { + try { + // call the method + const results = await pluginMethod.call(null, configuration, currentProject, version, user); + + // copy status into payload for ease of access in client + const response = { + ...results.result, + status: results.status, + }; + + // set status and return value as json + return res.status(results.status).json(response); + } catch (err) { + return res.status(400).json({ + statusCode: '400', + message: err.message, + }); + } + } + } + + res.status(400).json({ + statusCode: '400', + message: `${method} is not a valid publishing target type. There may be a missing plugin.`, + }); }, }; diff --git a/Composer/packages/server/src/models/settings/defaultSettingManager.ts b/Composer/packages/server/src/models/settings/defaultSettingManager.ts index 9d8c88d1cd..f54a653c6d 100644 --- a/Composer/packages/server/src/models/settings/defaultSettingManager.ts +++ b/Composer/packages/server/src/models/settings/defaultSettingManager.ts @@ -29,6 +29,7 @@ export class DefaultSettingManager extends FileSettingManager { defaultLanguage: 'en-us', environment: 'composer', }, + publishTargets: [], qna: { knowledgebaseid: '', endpointkey: '', diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index 8106d59115..cd09e765ef 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -37,7 +37,7 @@ router.get('/storages/:storageId/blobs', StorageController.getBlob); router.get('/publish/types', PublishController.getTypes); router.get('/publish/:projectId/status/:target', PublishController.status); router.post('/publish/:projectId/publish/:target', PublishController.publish); -router.post('/publish/:projectId/history/:target', PublishController.history); +router.get('/publish/:projectId/history/:target', PublishController.history); router.post('/publish/:projectId/rollback/:target', PublishController.rollback); router.get('/publish/:method', PublishController.publish); diff --git a/Composer/packages/server/src/services/pluginLoader.ts b/Composer/packages/server/src/services/pluginLoader.ts index b1931a8ad6..02f3322632 100644 --- a/Composer/packages/server/src/services/pluginLoader.ts +++ b/Composer/packages/server/src/services/pluginLoader.ts @@ -15,10 +15,12 @@ const passport = require('passport'); export class ComposerPluginRegistration { public loader: PluginLoader; private _name: string; + private _description: string; - constructor(loader: PluginLoader, name: string) { + constructor(loader: PluginLoader, name: string, description: string) { this.loader = loader; this._name = name; + this._description = description; } public get passport() { @@ -29,6 +31,14 @@ export class ComposerPluginRegistration { return this._name; } + public get description(): string { + return this._description; + } + + public set description(val: string) { + this._description = val; + } + /************************************************************************************** * Storage related features *************************************************************************************/ @@ -43,9 +53,12 @@ export class ComposerPluginRegistration { /************************************************************************************** * Publish related features *************************************************************************************/ - public async addPublishMethod(plugin: Partial) { + public async addPublishMethod(plugin: PublishPlugin) { console.log('registering publish method', this.name); - this.loader.extensions.publish[this.name] = plugin; + this.loader.extensions.publish[this.name] = { + plugin: this, + methods: plugin, + }; } /************************************************************************************** @@ -164,11 +177,26 @@ export class ComposerPluginRegistration { } } +interface PublishResult { + message: string; + comment?: string; + log?: string; + id?: string; + time?: Date; + endpointURL?: string; + status?: number; +} + +interface PublishResponse { + status: number; + result: PublishResult; +} + interface PublishPlugin { - publish: any; - getStatus?: any; - getHistory?: any; - rollback?: any; + publish: (config: any, project: any, metadata: any, user: any) => Promise; + getStatus?: (config: any, project: any, user: any) => Promise; + getHistory?: (config: any, project: any, user: any) => Promise; + rollback?: (config: any, project: any, rollbackToVersion: string, user: any) => Promise; [key: string]: any; } @@ -182,7 +210,10 @@ export class PluginLoader { [key: string]: any; }; publish: { - [key: string]: Partial; + [key: string]: { + plugin: ComposerPluginRegistration; + methods: PublishPlugin; + }; }; authentication: { middleware?: (req, res, next) => void; @@ -238,8 +269,8 @@ export class PluginLoader { }); } - public async loadPlugin(name: string, thisPlugin: any) { - const pluginRegistration = new ComposerPluginRegistration(this, name); + public async loadPlugin(name: string, description: string, thisPlugin: any) { + const pluginRegistration = new ComposerPluginRegistration(this, name, description); if (typeof thisPlugin.default === 'function') { // the module exported just an init function thisPlugin.default.call(null, pluginRegistration); @@ -263,7 +294,7 @@ export class PluginLoader { try { // eslint-disable-next-line security/detect-non-literal-require, @typescript-eslint/no-var-requires const thisPlugin = require(modulePath); - this.loadPlugin(json.name, thisPlugin); + this.loadPlugin(json.name, json.description, thisPlugin); } catch (err) { console.error(err); } diff --git a/Composer/plugins/README.md b/Composer/plugins/README.md index ebe5cd771b..d0730a0202 100644 --- a/Composer/plugins/README.md +++ b/Composer/plugins/README.md @@ -20,7 +20,7 @@ Plugins currently have access to the following functional areas: * Web server - plugins can add additional web routes to Composer's web server instance. * Publishing - plugins can add publishing mechanisms -Combining these three endpoints, it is possible to achieve scenarios such as: +Combining these endpoints, it is possible to achieve scenarios such as: * Store content in a database * Require login via AAD or any other oauth provider diff --git a/Composer/plugins/localPublish/package.json b/Composer/plugins/localPublish/package.json index 241352b8d6..11e1d12363 100644 --- a/Composer/plugins/localPublish/package.json +++ b/Composer/plugins/localPublish/package.json @@ -1,7 +1,7 @@ { "name": "localpublish", "version": "1.0.0", - "description": "", + "description": "start and update the local test bot runtime", "main": "lib/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/Composer/plugins/localPublish/src/index.ts b/Composer/plugins/localPublish/src/index.ts index ae99525ec3..cf3cc8bfd7 100644 --- a/Composer/plugins/localPublish/src/index.ts +++ b/Composer/plugins/localPublish/src/index.ts @@ -39,8 +39,8 @@ class LocalPublisher { constructor() { } // config include botId and version, project is content(ComposerDialogs) - publish = async (config: PublishConfig, project, user) => { - const { settings, templatePath } = config; + publish = async (config: PublishConfig, project, metadata, user) => { + const { templatePath, settings } = config; this.templatePath = templatePath; const botId = project.id; const version = 'default'; @@ -50,25 +50,49 @@ class LocalPublisher { return { status: 200, result: { - jobId: new uuid(), + id: uuid(), version: version, - endpoint: url, + endpointURL: url, }, }; }; - getStatus = async (botId: string) => { + getStatus = async (config: PublishConfig, project, user) => { + const botId = project.id; if (LocalPublisher.runningBots[botId]) { + const port = LocalPublisher.runningBots[botId].port; + const url = `http://localhost:${port}`; return { - botStatus: 'connected', + status: 200, + result: { + message: 'Running', + endpointURL: url, + }, }; } else { return { - botStatus: 'unConnected', + status: 200, + result: { + message: 'Ready', + }, }; } }; - history = async config => { }; - rollback = async (config, versionId) => { }; + history = async (config: PublishConfig, project, user) => { + const botId = project.id; + const result = []; + const files = await readDir(this.getHistoryDir(botId)); + console.log(files); + files.map(item => { + result.push({ + time: 'now', + status: 200, + message: 'test', + comment: 'test', + }); + }); + return result; + }; + rollback = async (config, project, versionId, user) => { }; private getBotsDir = () => process.env.LOCAL_PUBLISH_PATH || path.resolve(this.baseDir, 'hostedBots'); private getBotDir = (botId: string) => path.resolve(this.getBotsDir(), botId); diff --git a/Composer/plugins/mockRemotePublish/lib/index.d.ts b/Composer/plugins/mockRemotePublish/lib/index.d.ts new file mode 100644 index 0000000000..deae25e201 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/lib/index.d.ts @@ -0,0 +1,2 @@ +declare const _default: (composer: any) => Promise; +export default _default; diff --git a/Composer/plugins/mockRemotePublish/lib/index.js b/Composer/plugins/mockRemotePublish/lib/index.js new file mode 100644 index 0000000000..edc468e379 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/lib/index.js @@ -0,0 +1,121 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +/* This is a mock publishing endpoint that supports the basic features of a real remote publishing endpoint: + * - multiple profile can be configured to publish to this per bot + * - history will be maintained for each project/profile + * - status will initially be 202 to indicate in progress, will update after 10 seconds to 200 + */ +const uuid_1 = require("uuid"); +class LocalPublisher { + constructor() { + this.finishPublish = (botId, profileName, jobId) => __awaiter(this, void 0, void 0, function* () { + setTimeout(() => { + this.data[botId][profileName].forEach(element => { + if (element.result.id == jobId && element.status !== 500) { + element.status = 200; + element.result.message = 'Success'; + element.result.log = element.result.log + '\nPublish succeeded!'; + } + }); + }, 5000); + }); + // config include botId and version, project is content(ComposerDialogs) + this.publish = (config, project, metadata, user) => __awaiter(this, void 0, void 0, function* () { + const profileName = config.name; + if (!this.data[project.id]) { + this.data[project.id] = {}; + } + if (!this.data[project.id][profileName]) { + this.data[project.id][profileName] = []; + } + const response = { + status: 202, + result: { + time: new Date(), + message: 'Accepted for publishing.', + log: 'Publish starting...', + id: uuid_1.v4(), + comment: metadata.comment, + }, + }; + if (metadata.comment === '500') { + response.status = 500; + response.result.message = 'Failed'; + } + this.data[project.id][profileName].push(response); + this.finishPublish(project.id, profileName, response.result.id); + return response; + }); + this.getStatus = (config, project, user) => __awaiter(this, void 0, void 0, function* () { + const profileName = config.name; + const botId = project.id; + if (this.data[botId] && this.data[botId][profileName]) { + const response = this.data[botId][profileName][this.data[botId][profileName].length - 1]; + // return latest status + return response; + } + else { + return { + status: 404, + result: { + message: 'bot not published', + }, + }; + } + }); + this.history = (config, project, user) => __awaiter(this, void 0, void 0, function* () { + const profileName = config.name; + const botId = project.id; + const result = []; + if (this.data[botId] && this.data[botId][profileName]) { + this.data[botId][profileName].map(item => { + result.push(Object.assign(Object.assign({}, item.result), { status: item.status })); + }); + } + // return in reverse chrono + return result.reverse(); + }); + this.rollback = (config, project, rollbackToVersion, user) => __awaiter(this, void 0, void 0, function* () { + console.log('ROLLBACK TO', project.id, rollbackToVersion); + const profileName = config.name; + const botId = project.id; + console.log('eval list', this.data[botId][profileName]); + const matched = this.data[botId][profileName].filter(item => { + console.log('comparing ', item.result.id, rollbackToVersion); + return item.result.id === rollbackToVersion; + }); + if (matched.length && matched[0].status === 200) { + const rollback = Object.assign(Object.assign({}, matched[0]), { result: Object.assign({}, matched[0].result) }); + rollback.result.id = uuid_1.v4(); + this.data[botId][profileName].push(rollback); + return rollback; + } + else { + return { + status: 500, + result: { + message: 'No matching published version found in history', + }, + }; + } + }); + this.data = {}; + } +} +const publisher = new LocalPublisher(); +exports.default = (composer) => __awaiter(void 0, void 0, void 0, function* () { + // pass in the custom storage class that will override the default + yield composer.addPublishMethod(publisher); +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/lib/index.js.map b/Composer/plugins/mockRemotePublish/lib/index.js.map new file mode 100644 index 0000000000..012b1f5044 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,uCAAuC;AACvC,kCAAkC;;;;;;;;;;;AAElC;;;;GAIG;AAEH,+BAAkC;AAOlC,MAAM,cAAc;IAGlB;QAIA,kBAAa,GAAG,CAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE;YAClD,UAAU,CAAC,GAAG,EAAE;gBACd,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;oBAC9C,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,EAAE;wBACxD,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC;wBACrB,OAAO,CAAC,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;wBACnC,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,sBAAsB,CAAC;qBAClE;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,EAAE,IAAI,CAAC,CAAC;QACX,CAAC,CAAA,CAAC;QAEF,wEAAwE;QACxE,YAAO,GAAG,CAAO,MAAqB,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;YACjE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAEhC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;gBAC1B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC;aAC5B;YACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE;gBACvC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC;aACzC;YAED,MAAM,QAAQ,GAAG;gBACf,MAAM,EAAE,GAAG;gBACX,MAAM,EAAE;oBACN,IAAI,EAAE,IAAI,IAAI,EAAE;oBAChB,OAAO,EAAE,0BAA0B;oBACnC,GAAG,EAAE,qBAAqB;oBAC1B,EAAE,EAAE,SAAI,EAAE;oBACV,OAAO,EAAE,QAAQ,CAAC,OAAO;iBAC1B;aACF,CAAC;YAEF,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,EAAE;gBAC9B,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC;gBACtB,QAAQ,CAAC,MAAM,CAAC,OAAO,GAAG,QAAQ,CAAC;aACpC;YAED,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAElD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAEhE,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAA,CAAC;QACF,cAAS,GAAG,CAAO,MAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;YACzD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAChC,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC;YAEzB,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,EAAE;gBACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACzF,uBAAuB;gBACvB,OAAO,QAAQ,CAAC;aACjB;iBAAM;gBACL,OAAO;oBACL,MAAM,EAAE,GAAG;oBACX,MAAM,EAAE;wBACN,OAAO,EAAE,mBAAmB;qBAC7B;iBACF,CAAC;aACH;QACH,CAAC,CAAA,CAAC;QACF,YAAO,GAAG,CAAO,MAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;YACvD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAChC,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,EAAE;gBACrD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;oBACvC,MAAM,CAAC,IAAI,iCACN,IAAI,CAAC,MAAM,KACd,MAAM,EAAE,IAAI,CAAC,MAAM,IACnB,CAAC;gBACL,CAAC,CAAC,CAAC;aACJ;YACD,2BAA2B;YAC3B,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC,CAAA,CAAC;QACF,aAAQ,GAAG,CAAO,MAAqB,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE;YAC3E,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,EAAE,EAAE,iBAAiB,CAAC,CAAC;YAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAChC,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;YACxD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;gBAC1D,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,iBAAiB,CAAC,CAAC;gBAC7D,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,iBAAiB,CAAC;YAC9C,CAAC,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,GAAG,EAAE;gBAC/C,MAAM,QAAQ,mCAAQ,OAAO,CAAC,CAAC,CAAC,KAAE,MAAM,oBAAO,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC;gBACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,SAAI,EAAE,CAAC;gBAC5B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC7C,OAAO,QAAQ,CAAC;aACjB;iBAAM;gBACL,OAAO;oBACL,MAAM,EAAE,GAAG;oBACX,MAAM,EAAE;wBACN,OAAO,EAAE,gDAAgD;qBAC1D;iBACF,CAAC;aACH;QACH,CAAC,CAAA,CAAC;QAtGA,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,CAAC;CAsGF;AAED,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;AACvC,kBAAe,CAAO,QAAa,EAAiB,EAAE;IACpD,kEAAkE;IAClE,MAAM,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAC7C,CAAC,CAAA,CAAC"} \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/lib/interface.d.ts b/Composer/plugins/mockRemotePublish/lib/interface.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/plugins/mockRemotePublish/lib/interface.js b/Composer/plugins/mockRemotePublish/lib/interface.js new file mode 100644 index 0000000000..1ae9b6c207 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/lib/interface.js @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +//# sourceMappingURL=interface.js.map \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/lib/interface.js.map b/Composer/plugins/mockRemotePublish/lib/interface.js.map new file mode 100644 index 0000000000..088aaaf5d6 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/lib/interface.js.map @@ -0,0 +1 @@ +{"version":3,"file":"interface.js","sourceRoot":"","sources":["../src/interface.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC"} \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/package-lock.json b/Composer/plugins/mockRemotePublish/package-lock.json new file mode 100644 index 0000000000..b4adaac1d4 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/package-lock.json @@ -0,0 +1,368 @@ +{ + "name": "localpublish", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "adm-zip": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", + "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==" + }, + "archiver": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", + "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^2.6.3", + "buffer-crc32": "^0.2.1", + "glob": "^7.1.4", + "readable-stream": "^3.4.0", + "tar-stream": "^2.1.0", + "zip-stream": "^2.1.2" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "bl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.0.tgz", + "integrity": "sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==", + "requires": { + "readable-stream": "^3.0.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", + "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "compress-commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", + "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^3.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } + }, + "crc32-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", + "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", + "requires": { + "crc": "^3.4.4", + "readable-stream": "^3.4.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "tar-stream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.0.tgz", + "integrity": "sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==", + "requires": { + "bl": "^3.0.0", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.1.tgz", + "integrity": "sha512-yqjRXZzSJm9Dbl84H2VDHpM3zMjzSJQ+hn6C4zqd5ilW+7P4ZmLEEqwho9LjP+tGuZlF4xrHQXT0h9QZUS/pWA==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "zip-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", + "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^2.1.1", + "readable-stream": "^3.4.0" + } + } + } +} diff --git a/Composer/plugins/mockRemotePublish/package.json b/Composer/plugins/mockRemotePublish/package.json new file mode 100644 index 0000000000..5c58840214 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/package.json @@ -0,0 +1,22 @@ +{ + "name": "mockRemotePublish", + "version": "1.0.0", + "description": "publish assets to a remote URL", + "main": "lib/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "extendsComposer": false, + "author": "", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.4.14", + "archiver": "^3.1.1", + "globby": "^11.0.0", + "path": "^0.12.7", + "portfinder": "^1.0.25", + "rimraf": "^3.0.2", + "uuid": "^7.0.1" + } +} \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/src/index.ts b/Composer/plugins/mockRemotePublish/src/index.ts new file mode 100644 index 0000000000..62ae653848 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/src/index.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* This is a mock publishing endpoint that supports the basic features of a real remote publishing endpoint: + * - multiple profile can be configured to publish to this per bot + * - history will be maintained for each project/profile + * - status will initially be 202 to indicate in progress, will update after 10 seconds to 200 + */ + +import { v4 as uuid } from 'uuid'; + +interface PublishConfig { + name: string; + settings: any; +} + +class LocalPublisher { + private data: any; + + constructor() { + this.data = {}; + } + + finishPublish = async (botId, profileName, jobId) => { + setTimeout(() => { + this.data[botId][profileName].forEach(element => { + if (element.result.id == jobId && element.status !== 500) { + element.status = 200; + element.result.message = 'Success'; + element.result.log = element.result.log + '\nPublish succeeded!'; + } + }); + }, 5000); + }; + + // config include botId and version, project is content(ComposerDialogs) + publish = async (config: PublishConfig, project, metadata, user) => { + const profileName = config.name; + + if (!this.data[project.id]) { + this.data[project.id] = {}; + } + if (!this.data[project.id][profileName]) { + this.data[project.id][profileName] = []; + } + + const response = { + status: 202, + result: { + time: new Date(), + message: 'Accepted for publishing.', + log: 'Publish starting...', + id: uuid(), + comment: metadata.comment, + }, + }; + + if (metadata.comment === '500') { + response.status = 500; + response.result.message = 'Failed'; + } + + this.data[project.id][profileName].push(response); + + this.finishPublish(project.id, profileName, response.result.id); + + return response; + }; + getStatus = async (config: PublishConfig, project, user) => { + const profileName = config.name; + const botId = project.id; + + if (this.data[botId] && this.data[botId][profileName]) { + const response = this.data[botId][profileName][this.data[botId][profileName].length - 1]; + // return latest status + return response; + } else { + return { + status: 404, + result: { + message: 'bot not published', + }, + }; + } + }; + history = async (config: PublishConfig, project, user) => { + const profileName = config.name; + const botId = project.id; + const result = []; + if (this.data[botId] && this.data[botId][profileName]) { + this.data[botId][profileName].map(item => { + result.push({ + ...item.result, + status: item.status, + }); + }); + } + // return in reverse chrono + return result.reverse(); + }; + rollback = async (config: PublishConfig, project, rollbackToVersion, user) => { + console.log('ROLLBACK TO', project.id, rollbackToVersion); + const profileName = config.name; + const botId = project.id; + console.log('eval list', this.data[botId][profileName]); + const matched = this.data[botId][profileName].filter(item => { + console.log('comparing ', item.result.id, rollbackToVersion); + return item.result.id === rollbackToVersion; + }); + if (matched.length && matched[0].status === 200) { + const rollback = { ...matched[0], result: { ...matched[0].result } }; + rollback.result.id = uuid(); + this.data[botId][profileName].push(rollback); + return rollback; + } else { + return { + status: 500, + result: { + message: 'No matching published version found in history', + }, + }; + } + }; +} + +const publisher = new LocalPublisher(); +export default async (composer: any): Promise => { + // pass in the custom storage class that will override the default + await composer.addPublishMethod(publisher); +}; diff --git a/Composer/plugins/mockRemotePublish/src/interface.ts b/Composer/plugins/mockRemotePublish/src/interface.ts new file mode 100644 index 0000000000..fc36ab244f --- /dev/null +++ b/Composer/plugins/mockRemotePublish/src/interface.ts @@ -0,0 +1,2 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. diff --git a/Composer/plugins/mockRemotePublish/tsconfig.json b/Composer/plugins/mockRemotePublish/tsconfig.json new file mode 100644 index 0000000000..d43fd5f5f0 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "outDir": "./lib", + "rootDir": "./src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/Composer/plugins/mockRemotePublish/yarn.lock b/Composer/plugins/mockRemotePublish/yarn.lock new file mode 100644 index 0000000000..e01b269694 --- /dev/null +++ b/Composer/plugins/mockRemotePublish/yarn.lock @@ -0,0 +1,538 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +adm-zip@^0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.14.tgz#2cf312bcc9f8875df835b0f6040bd89be0a727a9" + integrity sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g== + +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0" + integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg== + dependencies: + archiver-utils "^2.1.0" + async "^2.6.3" + buffer-crc32 "^0.2.1" + glob "^7.1.4" + readable-stream "^3.4.0" + tar-stream "^2.1.0" + zip-stream "^2.1.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +async@^2.6.2, async@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +bl@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" + integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + dependencies: + readable-stream "^3.0.1" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer@^5.1.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +compress-commons@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610" + integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^3.0.1" + normalize-path "^3.0.0" + readable-stream "^2.3.6" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +crc32-stream@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85" + integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w== + dependencies: + crc "^3.4.4" + readable-stream "^3.4.0" + +crc@^3.4.4: + version "3.8.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== + dependencies: + buffer "^5.1.0" + +debug@^3.1.1: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +fast-glob@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fastq@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" + integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +glob-parent@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" + integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +graceful-fs@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ignore@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= + dependencies: + readable-stream "^2.0.5" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= + +lodash@^4.17.14: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= + dependencies: + process "^0.11.1" + util "^0.10.3" + +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== + +portfinder@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.1: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +tar-stream@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" + integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +uuid@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.1.tgz#95ed6ff3d8c881cbf85f0f05cc3915ef994818ef" + integrity sha512-yqjRXZzSJm9Dbl84H2VDHpM3zMjzSJQ+hn6C4zqd5ilW+7P4ZmLEEqwho9LjP+tGuZlF4xrHQXT0h9QZUS/pWA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +zip-stream@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b" + integrity sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q== + dependencies: + archiver-utils "^2.1.0" + compress-commons "^2.1.1" + readable-stream "^3.4.0"