diff --git a/Composer/cypress/integration/Publish.spec.ts b/Composer/cypress/integration/Publish.spec.ts deleted file mode 100644 index 9166d7e4ac..0000000000 --- a/Composer/cypress/integration/Publish.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -context('Publish Page', () => { - beforeEach(() => { - cy.server(); - cy.route('POST', '/api/publish/*/publish/*', { - status: 202, - message: 'Accepted for publishing.', - time: new Date(), - log: '', - comment: '', - }); - cy.route('PUT', '/api/projects/*/files/*', 'OK'); - cy.route('GET', '/api/publish/*/status/*', { - status: 200, - message: 'Success', - time: new Date(), - log: '', - comment: '', - }); - cy.route('GET', '/api/publish/types', [ - { - name: 'azurePublish', - description: 'azure publish', - instructions: 'plugin instruction', - schema: { - default: { - test: 'test', - }, - }, - features: { - history: true, - publish: true, - status: true, - rollback: true, - }, - }, - ]); - cy.route('GET', '/api/publish/*/history/*', []); - cy.visit('/home'); - cy.createBot('EchoBot'); - }); - it('can add profile and publish in publish page', () => { - // click left nav button - cy.findByTestId('LeftNav-CommandBarButtonPublish').click(); - cy.findByText('__TestEchoBot'); - - cy.contains('Bot'); - cy.contains('Date'); - cy.visitPage('Project Settings'); - cy.findByText('Add new publish profile').click(); - cy.findByText('Add a publish profile').should('exist'); - cy.findAllByPlaceholderText('My Publish Profile').first().type('testProfile'); - cy.findByText('Choose One').click(); - cy.findByText('azure publish').click(); - // save profile - cy.findByText('Save').click(); - - cy.findByTestId('LeftNav-CommandBarButtonPublish').click(); - cy.findByText('testProfile'); - }); -}); diff --git a/Composer/package.json b/Composer/package.json index f219bf82d9..87eba20892 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -126,4 +126,4 @@ "wait-on": "^5.2.0", "wsrun": "^5.2.0" } -} +} \ No newline at end of file diff --git a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx index b561ee9867..c834cca426 100644 --- a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx +++ b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx @@ -47,6 +47,7 @@ const rootState = { status: true, rollback: true, pull: true, + provision: true, }, }, ], diff --git a/Composer/packages/client/config/env.js b/Composer/packages/client/config/env.js index 3172433e7f..e14564c670 100644 --- a/Composer/packages/client/config/env.js +++ b/Composer/packages/client/config/env.js @@ -91,6 +91,9 @@ function getClientEnvironment(publicUrl) { COMPOSER_VERSION: '1.3.0-rc4', LOCAL_PUBLISH_PATH: process.env.LOCAL_PUBLISH_PATH || path.resolve(process.cwd(), '../../../extensions/localPublish/hostedBots'), + WEBLOGIN_CLIENTID: process.env.WEBLOGIN_CLIENTID, + WEBLOGIN_TENANTID: process.env.WEBLOGIN_TENANTID, + WEBLOGIN_REDIRECTURL: process.env.WEBLOGIN_REDIRECTURL, } ); // Stringify all values so we can feed into Webpack DefinePlugin diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index c4b5d9df6d..b88b362254 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -25,7 +25,6 @@ "@bfc/form-dialogs": "*", "@bfc/indexers": "*", "@bfc/shared": "*", - "@bfc/ui-shared": "*", "@bfc/ui-plugin-composer": "*", "@bfc/ui-plugin-cross-trained": "*", "@bfc/ui-plugin-dialog-schema-editor": "*", @@ -47,6 +46,7 @@ "format-message": "^6.2.3", "format-message-generate-id": "^6.2.3", "immer": "^5.2.0", + "jsonwebtoken": "^8.5.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.19", "office-ui-fabric-react": "^7.121.11", diff --git a/Composer/packages/client/src/components/Auth/AuthDialog.tsx b/Composer/packages/client/src/components/Auth/AuthDialog.tsx new file mode 100644 index 0000000000..d6b398d741 --- /dev/null +++ b/Composer/packages/client/src/components/Auth/AuthDialog.tsx @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; +import formatMessage from 'format-message'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { useCallback, useState } from 'react'; + +import storage from '../../utils/storage'; +import { isTokenExpired } from '../../utils/auth'; + +export interface AuthDialogProps { + needGraph: boolean; + onDismiss: () => void; + next: () => void; +} +export const AuthDialog: React.FC = (props) => { + const [graphToken, setGraphToken] = useState(''); + const [accessToken, setAccessToken] = useState(''); + const [tokenError, setTokenError] = useState(''); + const [graphError, setGraphError] = useState(''); + const isAble = useCallback(() => { + if (!accessToken) { + return false; + } else if (tokenError) { + return false; + } + + if (props.needGraph && (!graphToken || graphError)) { + return false; + } + return true; + }, [accessToken, graphToken]); + + return ( + + { + newValue && setAccessToken(newValue); + if (isTokenExpired(newValue || '')) { + setTokenError('Token Expire or token invalid'); + } else { + setTokenError(''); + } + }} + /> + {props.needGraph ? ( + { + newValue && setGraphToken(newValue); + if (isTokenExpired(newValue || '')) { + setGraphError('Token Expire or token invalid'); + } else { + setGraphError(''); + } + }} + /> + ) : null} + + + { + props.onDismiss(); + // cache tokens + storage.set('accessToken', accessToken); + storage.set('graphToken', graphToken); + props.next(); + }} + /> + + + ); +}; diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index fcc22c6faf..dc8d5f10d8 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -337,6 +337,28 @@ export const QnABotTemplateId = 'QnASample'; export const nameRegex = /^[a-zA-Z0-9-_]+$/; +export const authConfig = { + // for web login + clientId: process.env.WEBLOGIN_CLIENTID, + scopes: [ + 'https://management.core.windows.net/user_impersonation', + 'https://graph.microsoft.com/Application.ReadWrite.All', + ], + tenantId: process.env.WEBLOGIN_TENANTID, + redirectUrl: process.env.WEBLOGIN_REDIRECTURL, +}; + +export const armScopes = { + scopes: ['https://management.core.windows.net/user_impersonation'], + targetResource: 'https://management.core.windows.net/', +}; +export const graphScopes = { + scopes: ['https://graph.microsoft.com/Application.ReadWrite.All'], + targetResource: 'https://graph.microsoft.com/', +}; + +export const authUrl = `https://login.microsoftonline.com/${authConfig.tenantId}/oauth2/v2.0/authorize`; + export const triggerNotSupportedWarning = () => formatMessage( 'This trigger type is not supported by the RegEx recognizer. To ensure this trigger is fired, change the recognizer type.' diff --git a/Composer/packages/client/src/pages/botProject/PublishTargets.tsx b/Composer/packages/client/src/pages/botProject/PublishTargets.tsx index 3e26e9166c..d7b2497eb2 100644 --- a/Composer/packages/client/src/pages/botProject/PublishTargets.tsx +++ b/Composer/packages/client/src/pages/botProject/PublishTargets.tsx @@ -2,21 +2,19 @@ // Licensed under the MIT License. /** @jsx jsx */ -import React, { Fragment, useState, useCallback, useEffect } from 'react'; +import React, { Fragment, useState, useEffect } from 'react'; import { jsx, css } from '@emotion/core'; import { useRecoilValue } from 'recoil'; import { PublishTarget } from '@bfc/shared'; import formatMessage from 'format-message'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; -import { DialogType } from 'office-ui-fabric-react/lib/Dialog'; import { FontSizes, FontWeights } from 'office-ui-fabric-react/lib/Styling'; import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; -import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; import { dispatcherState, settingsState, publishTypesState } from '../../recoilModel'; import { CollapsableWrapper } from '../../components/CollapsableWrapper'; -import { CreatePublishTarget } from '../publish/createPublishTarget'; -import TelemetryClient from '../../telemetry/TelemetryClient'; + +import { PublishProfileDialog } from './create-publish-profile/PublishProfileDialog'; // -------------------- Styles -------------------- // @@ -106,92 +104,11 @@ export const PublishTargets: React.FC = (props) => { const { publishTargets } = useRecoilValue(settingsState(projectId)); const { getPublishTargetTypes, setPublishTargets } = useRecoilValue(dispatcherState); const publishTypes = useRecoilValue(publishTypesState(projectId)); - const [editTarget, setEditTarget] = useState<{ index: number; item: PublishTarget } | null>(null); - const [editDialogProps, setEditDialogProps] = useState({ - title: formatMessage('Title'), - type: DialogType.normal, - children: {}, - }); - - const [dialogProps, setDialogProps] = useState({ - title: formatMessage('Title'), - type: DialogType.normal, - children: {}, - }); - const [addDialogHidden, setAddDialogHidden] = useState(true); - const [editDialogHidden, setEditDialogHidden] = useState(true); + const [dialogHidden, setDialogHidden] = useState(true); const publishTargetsRef = React.useRef(null); - - const onEdit = useCallback( - async (index: number, item: PublishTarget) => { - const newItem = { item: item, index: index }; - setEditTarget(newItem); - setEditDialogHidden(false); - }, - [publishTargets] - ); - - const updatePublishTarget = useCallback( - async (name: string, type: string, configuration: string) => { - if (!editTarget) { - return; - } - - const targets = publishTargets ? [...publishTargets] : []; - - targets[editTarget.index] = { - name, - type, - configuration, - }; - - await setPublishTargets(targets, projectId); - }, - [publishTargets, projectId, editTarget] - ); - - const savePublishTarget = useCallback( - async (name: string, type: string, configuration: string) => { - const targets = [...(publishTargets || []), { name, type, configuration }]; - await setPublishTargets(targets, projectId); - TelemetryClient.track('NewPublishingProfileSaved', { type }); - }, - [publishTargets, projectId] - ); - - useEffect(() => { - setDialogProps({ - title: formatMessage('Add a publish profile'), - type: DialogType.normal, - children: ( - setAddDialogHidden(true)} - current={null} - targets={publishTargets || []} - types={publishTypes} - updateSettings={savePublishTarget} - /> - ), - }); - }, [publishTypes, savePublishTarget, publishTargets]); - - useEffect(() => { - setEditDialogProps({ - title: formatMessage('Edit a publish profile'), - type: DialogType.normal, - children: ( - setEditDialogHidden(true)} - current={editTarget ? editTarget.item : null} - targets={(publishTargets || []).filter((item) => editTarget && item.name !== editTarget.item.name)} - types={publishTypes} - updateSettings={updatePublishTarget} - /> - ), - }); - }, [editTarget, publishTypes, updatePublishTarget]); + const [current, setCurrent] = useState<{ index: number; item: PublishTarget } | null>(null); useEffect(() => { if (projectId) { @@ -224,7 +141,13 @@ export const PublishTargets: React.FC = (props) => { {p.type}
- await onEdit(index, p)}> + { + setCurrent({ item: p, index: index }); + setDialogHidden(false); + }} + > {formatMessage('Edit')}
@@ -234,33 +157,26 @@ export const PublishTargets: React.FC = (props) => { { - setAddDialogHidden(false); - TelemetryClient.track('NewPublishingProfileStarted'); - }} + onClick={() => setDialogHidden(false)} > {formatMessage('Add new publish profile')} - setAddDialogHidden(true)} - > - {dialogProps.children} - - setEditDialogHidden(true)} - > - {editDialogProps.children} - + {!dialogHidden ? ( + { + setDialogHidden(true); + // reset current + setCurrent(null); + }} + current={current} + projectId={projectId} + setPublishTargets={setPublishTargets} + targets={publishTargets || []} + types={publishTypes} + /> + ) : null} ); }; diff --git a/Composer/packages/client/src/pages/botProject/create-publish-profile/AddProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/create-publish-profile/AddProfileDialog.tsx new file mode 100644 index 0000000000..54eebcedeb --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/AddProfileDialog.tsx @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import formatMessage from 'format-message'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { useState, useMemo, useCallback, Fragment, useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { Separator } from 'office-ui-fabric-react/lib/Separator'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { PublishTarget } from '@bfc/shared'; + +import { separator } from '../../publish/styles'; +import { armScopes, graphScopes } from '../../../constants'; +import { PublishType } from '../../../recoilModel/types'; +import { isShowAuthDialog } from '../../../utils/auth'; +import { AuthDialog } from '../../../components/Auth/AuthDialog'; +import { PluginAPI } from '../../../plugins/api'; +import { dispatcherState } from '../../../recoilModel'; +import { AuthClient } from '../../../utils/authClient'; +import { getTokenFromCache, isGetTokenFromUser } from '../../../utils/auth'; + +type AddProfileDialogProps = { + onDismiss: () => void; + targets: PublishTarget[]; + types: PublishType[]; + onNext: () => void; + updateSettings: (name: string, type: string, configuration: string) => Promise; + projectId: string; + setType: (value) => void; +}; + +export const AddProfileDialog: React.FC = (props) => { + const { onDismiss, targets, types, onNext, updateSettings, projectId, setType } = props; + const [name, setName] = useState(''); + const [errorMessage, setErrorMsg] = useState(''); + const [targetType, setTargetType] = useState(''); + const [showAuthDialog, setShowAuthDialog] = useState(false); + const { provisionToTarget } = useRecoilValue(dispatcherState); + + const updateName = (e, newName) => { + setName(newName); + isValidateProfileName(newName); + }; + + const isValidateProfileName = (newName) => { + if (!newName || newName.trim() === '') { + setErrorMsg(formatMessage('Must have a name')); + } else { + const exists = !!targets?.some((t) => t.name.toLowerCase() === newName?.toLowerCase()); + + if (exists) { + setErrorMsg(formatMessage('A profile with that name already exists.')); + } else { + setErrorMsg(''); + } + } + }; + + const targetTypes = useMemo(() => { + return types.map((t) => ({ key: t.name, text: t.description })); + }, [types]); + + const updateType = useCallback( + (_e, option?: IDropdownOption) => { + const type = types.find((t) => t.name === option?.key); + setType(type); + if (type) { + setTargetType(type.name); + } + }, + [types] + ); + + const saveDisabled = useMemo(() => { + return !targetType || !name || !!errorMessage; + }, [errorMessage, name, targetType]); + + // pass functions to extensions + useEffect(() => { + PluginAPI.publish.getType = () => { + return targetType; + }; + PluginAPI.publish.getSchema = () => { + return types.find((t) => t.name === targetType)?.schema; + }; + PluginAPI.publish.savePublishConfig = (config) => { + updateSettings(name, targetType, JSON.stringify(config) || '{}'); + }; + }, [targetType, name, types, updateSettings]); + + useEffect(() => { + PluginAPI.publish.startProvision = async (config) => { + const fullConfig = { ...config, name: name, type: targetType }; + let arm, graph; + if (!isGetTokenFromUser()) { + // login or get token implicit + arm = await AuthClient.getAccessToken(armScopes); + graph = await AuthClient.getAccessToken(graphScopes); + } else { + // get token from cache + arm = getTokenFromCache('accessToken'); + graph = getTokenFromCache('graphToken'); + } + provisionToTarget(fullConfig, config.type, projectId, arm, graph); + }; + }, [name, targetType]); + + return ( + + {showAuthDialog && ( + { + onNext(); + }} + onDismiss={() => { + setShowAuthDialog(false); + }} + /> + )} + +
+
+ + + +
+ + + + { + if (isShowAuthDialog(true)) { + setShowAuthDialog(true); + } else { + onNext(); + } + }} + /> + +
+
+ ); +}; diff --git a/Composer/packages/client/src/pages/botProject/create-publish-profile/EditProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/create-publish-profile/EditProfileDialog.tsx new file mode 100644 index 0000000000..8dfa003210 --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/EditProfileDialog.tsx @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import formatMessage from 'format-message'; +import { useState, useCallback, useMemo } from 'react'; +import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; +import { Separator } from 'office-ui-fabric-react/lib/Separator'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { PublishTarget } from '@bfc/shared'; +import { JsonEditor } from '@bfc/code-editor'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { useRecoilValue } from 'recoil'; + +import { PublishType } from '../../../recoilModel/types'; +import { label, separator } from '../../publish/styles'; +import { userSettingsState } from '../../../recoilModel'; + +type EditProfileDialogProps = { + onDismiss: () => void; + current: { index: number; item: PublishTarget } | null; + types: PublishType[]; + updateSettings: (name: string, type: string, configuration: string, editTarget: any) => Promise; +}; + +export const EditProfileDialog: React.FC = (props) => { + const { current, onDismiss, types, updateSettings } = props; + const [targetType, setTargetType] = useState(current?.item.type || ''); + const userSettings = useRecoilValue(userSettingsState); + const [config, setConfig] = useState(current ? JSON.parse(current.item.configuration) : undefined); + + const targetTypes = useMemo(() => { + return types.map((t) => ({ key: t.name, text: t.description })); + }, [types]); + + const updateType = useCallback( + (_e, option?: IDropdownOption) => { + const type = targetTypes.find((t) => t.key === option?.key); + + if (type) { + setTargetType(type.key); + } + }, + [targetTypes] + ); + + const selectedType = useMemo(() => { + return types.find((t) => t.name === targetType); + }, [types, targetType]); + + const submit = useCallback( + (_e) => { + if (targetType) { + updateSettings(current?.item.name || '', targetType, JSON.stringify(config) || '{}', current); + onDismiss(); + } + }, + [targetType, config, current, updateSettings] + ); + + return ( + +
+
+ + + + {selectedType?.instructions &&

{selectedType.instructions}

} +
{formatMessage('Publish Configuration')}
+ +
+ + + + + +
+ ); +}; diff --git a/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx new file mode 100644 index 0000000000..a5b1f7edab --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { Fragment, useState, useEffect, useCallback } from 'react'; +import { PublishTarget } from '@bfc/shared'; +import formatMessage from 'format-message'; +import { DialogWrapper, DialogTypes } from '@bfc/ui-shared'; + +import { getTokenFromCache, isGetTokenFromUser } from '../../../utils/auth'; +import { PublishType } from '../../../recoilModel/types'; +import { PluginAPI } from '../../../plugins/api'; +import { PluginHost } from '../../../components/PluginHost/PluginHost'; +import { defaultPublishSurface, pvaPublishSurface, azurePublishSurface } from '../../publish/styles'; +import TelemetryClient from '../../../telemetry/TelemetryClient'; + +import { EditProfileDialog } from './EditProfileDialog'; +import { AddProfileDialog } from './AddProfileDialog'; + +type PublishProfileDialogProps = { + closeDialog: () => void; + current: { index: number; item: PublishTarget } | null; + targets: PublishTarget[]; + types: PublishType[]; + projectId: string; + setPublishTargets: (targets: PublishTarget[], projectId: string) => Promise; +}; + +enum Page { + AddProfile = 'add', + EditProfile = 'edit', + ConfigProvision = 'config', +} + +export const PublishProfileDialog: React.FC = (props) => { + const { current, types, projectId, closeDialog, targets, setPublishTargets } = props; + const [page, setPage] = useState(current ? Page.EditProfile : Page.AddProfile); + const [publishSurfaceStyles, setStyles] = useState(defaultPublishSurface); + + const [dialogTitle, setTitle] = useState({ + title: formatMessage('Add a publish profile'), + subText: formatMessage( + 'Publish profile informs your bot where to use resources from. The resoruces you provision for your bot will live within this profile' + ), + }); + + const [selectedType, setSelectType] = useState(); + + useEffect(() => { + const ty = types.find((t) => t.name === current?.item.type); + setSelectType(ty); + }, [types, current]); + + useEffect(() => { + if (selectedType?.bundleId) { + // render custom plugin view + switch (selectedType.extensionId) { + case 'pva-publish-composer': + setStyles(pvaPublishSurface); + break; + case 'azurePublish': + setStyles(azurePublishSurface); + break; + default: + setStyles(defaultPublishSurface); + break; + } + } + }, [selectedType]); + + // setup plugin APIs + useEffect(() => { + PluginAPI.publish.closeDialog = closeDialog; + PluginAPI.publish.onBack = () => { + setPage(Page.AddProfile); + }; + PluginAPI.publish.getTokenFromCache = () => { + return { + accessToken: getTokenFromCache('accessToken'), + graphToken: getTokenFromCache('graphToken'), + }; + }; + PluginAPI.publish.isGetTokenFromUser = () => { + return isGetTokenFromUser(); + }; + PluginAPI.publish.setTitle = (value) => { + setTitle(value); + }; + }, []); + + // setup plugin APIs so that the provisioning plugin can initiate the process from inside the iframe + useEffect(() => { + PluginAPI.publish.useConfigBeingEdited = () => [current ? JSON.parse(current.item.configuration) : undefined]; + PluginAPI.publish.currentProjectId = () => { + return projectId; + }; + }, [current, projectId]); + + const updatePublishTarget = useCallback( + async (name: string, type: string, configuration: string, editTarget: any) => { + if (!editTarget) { + return; + } + + const newTargets = targets ? [...targets] : []; + newTargets[editTarget.index] = { + name, + type, + configuration, + }; + + await setPublishTargets(newTargets, projectId); + }, + [targets, projectId] + ); + + const savePublishTarget = useCallback( + async (name: string, type: string, configuration: string) => { + const newTargets = [...(targets || []), { name, type, configuration }]; + await setPublishTargets(newTargets, projectId); + TelemetryClient.track('NewPublishingProfileSaved', { type }); + }, + [targets, projectId] + ); + + return ( + + {page === Page.EditProfile && ( + + )} + {page != Page.EditProfile && ( + + {page === Page.AddProfile && ( + { + setPage(Page.ConfigProvision); + }} + /> + )} + {page === Page.ConfigProvision && selectedType?.bundleId && ( +
+ +
+ )} +
+ )} +
+ ); +}; diff --git a/Composer/packages/client/src/pages/publish/BotStatusList.tsx b/Composer/packages/client/src/pages/publish/BotStatusList.tsx index e5a4372a77..51b4799627 100644 --- a/Composer/packages/client/src/pages/publish/BotStatusList.tsx +++ b/Composer/packages/client/src/pages/publish/BotStatusList.tsx @@ -10,7 +10,7 @@ import { Icon } from 'office-ui-fabric-react/lib/Icon'; import React, { useState, Fragment } from 'react'; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; -import { PublishTarget } from '@bfc/shared'; +import { PublishTarget, PublishResult } from '@bfc/shared'; import { CheckboxVisibility, DetailsList, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { SharedColors } from '@uifabric/fluent-theme'; @@ -19,7 +19,7 @@ import { FontSizes } from '@uifabric/styling'; import { navigateTo } from '../../utils/navigation'; import { PublishType } from '../../recoilModel/types'; -import { IStatus, PublishStatusList } from './PublishStatusList'; +import { PublishStatusList } from './PublishStatusList'; import { detailList, listRoot, tableView } from './styles'; export type IBotStatus = { @@ -35,15 +35,15 @@ export type IBotStatus = { export type IBotStatusListProps = { projectId: string; items: IBotStatus[]; - botPublishHistoryList: { projectId: string; publishHistory: { [key: string]: IStatus[] } }[]; + botPublishHistoryList: { projectId: string; publishHistory: { [key: string]: PublishResult[] } }[]; botPublishTypesList: { projectId: string; publishTypes: PublishType[] }[]; publishDisabled: boolean; updateItems: (items: IBotStatus[]) => void; - updatePublishHistory: (items: IStatus[], item: IBotStatus) => void; + updatePublishHistory: (items: PublishResult[], item: IBotStatus) => void; updateSelectedBots: (items: IBotStatus[]) => void; changePublishTarget: (PublishTarget: string, item: IBotStatus) => void; - onLogClick: (item: IStatus) => void; - onRollbackClick: (selectedVersion: IStatus, item: IBotStatus) => void; + onLogClick: (item: PublishResult) => void; + onRollbackClick: (selectedVersion: PublishResult, item: IBotStatus) => void; }; export const BotStatusList: React.FC = (props) => { @@ -288,7 +288,7 @@ export const BotStatusList: React.FC = (props) => { ]; const onRenderRow = (props, defaultRender) => { const { item }: { item: IBotStatus } = props; - const publishStatusList: IStatus[] = item.publishTarget + const publishStatusList: PublishResult[] = item.publishTarget ? botPublishHistoryList.find((list) => list.projectId === item.id)?.publishHistory[item.publishTarget] || [] : []; const target = item.publishTargets?.find((target) => target.name === item.publishTarget); diff --git a/Composer/packages/client/src/pages/publish/Publish.tsx b/Composer/packages/client/src/pages/publish/Publish.tsx index 56534b0559..721438456e 100644 --- a/Composer/packages/client/src/pages/publish/Publish.tsx +++ b/Composer/packages/client/src/pages/publish/Publish.tsx @@ -10,19 +10,22 @@ import { Dialog } from 'office-ui-fabric-react/lib/Dialog'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { useRecoilValue } from 'recoil'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; -import { DialogSetting, PublishTarget } from '@bfc/shared'; +import { DialogSetting, PublishTarget, PublishResult } from '@bfc/shared'; import isEqual from 'lodash/isEqual'; import { dispatcherState, localBotPublishHistorySelector, localBotsDataSelector } from '../../recoilModel'; import { Toolbar, IToolbarItem } from '../../components/Toolbar'; +import { AuthDialog } from '../../components/Auth/AuthDialog'; import { createNotification } from '../../recoilModel/dispatchers/notification'; import { Notification, PublishType } from '../../recoilModel/types'; import { getSensitiveProperties } from '../../recoilModel/dispatchers/utils/project'; +import { armScopes } from '../../constants'; +import { getTokenFromCache, isShowAuthDialog, isGetTokenFromUser } from '../../utils/auth'; +import { AuthClient } from '../../utils/authClient'; import TelemetryClient from '../../telemetry/TelemetryClient'; import { PublishDialog } from './PublishDialog'; import { ContentHeaderStyle, HeaderText, ContentStyle, contentEditor } from './styles'; -import { IStatus } from './PublishStatusList'; import { BotStatusList, IBotStatus } from './BotStatusList'; import { getPendingNotificationCardProps, getPublishedNotificationCardProps } from './Notifications'; import { PullDialog } from './pullDialog'; @@ -107,14 +110,16 @@ const Publish: React.FC(statusList); const [botPublishHistoryList, setBotPublishHistoryList] = useState< - { projectId: string; publishHistory: { [key: string]: IStatus[] } }[] + { projectId: string; publishHistory: { [key: string]: PublishResult[] } }[] >(publishHistoryList); const [showLog, setShowLog] = useState(false); const [publishDialogHidden, setPublishDialogHidden] = useState(true); const [pullDialogHidden, setPullDialogHidden] = useState(true); + const [showAuthDialog, setShowAuthDialog] = useState(false); + // items to show in the list - const [selectedVersion, setSelectedVersion] = useState(null); + const [selectedVersion, setSelectedVersion] = useState(null); const isPullSupported = useMemo(() => { return !!selectedBots.find((bot) => { @@ -138,7 +143,13 @@ const Publish: React.FC setPublishDialogHidden(false)} + onClick={() => { + if (isShowAuthDialog(false)) { + setShowAuthDialog(true); + } else { + setPublishDialogHidden(false); + } + }} > { + const rollbackToVersion = (version: PublishResult, item: IBotStatus) => { const setting = botSettingList.find((botSetting) => botSetting.projectId === item.id)?.setting; const selectedTarget = item.publishTargets?.find((target) => target.name === item.publishTarget); if (setting) { @@ -309,7 +320,7 @@ const Publish: React.FC { + const onRollbackToVersion = (selectedVersion: PublishResult, item: IBotStatus) => { item.publishTarget && item.publishTargets && rollbackToVersion(selectedVersion, item); }; const onShowLog = (selectedVersion) => { @@ -319,7 +330,7 @@ const Publish: React.FC { setBotStatusList(statusList); }; - const updatePublishHistory = (publishHistories: IStatus[], botStatus: IBotStatus) => { + const updatePublishHistory = (publishHistories: PublishResult[], botStatus: IBotStatus) => { const newPublishHistory = botPublishHistoryList.map((botPublishHistory) => { if (botPublishHistory.projectId === botStatus.id && botStatus.publishTarget) { botPublishHistory.publishHistory = { @@ -344,6 +355,15 @@ const Publish: React.FC { + // get token + let token = ''; + if (isGetTokenFromUser()) { + token = getTokenFromCache('accessToken'); + console.log(token); + } else { + token = await AuthClient.getAccessToken(armScopes); + } + setPublishDisabled(true); setPreviousBotPublishHistoryList(botPublishHistoryList); // notifications @@ -368,7 +388,7 @@ const Publish: React.FC { @@ -421,6 +441,15 @@ const Publish: React.FC + {showAuthDialog && ( + setPublishDialogHidden(false)} + onDismiss={() => { + setShowAuthDialog(false); + }} + /> + )} {!publishDialogHidden && ( setPublishDialogHidden(true)} onSubmit={publish} /> )} diff --git a/Composer/packages/client/src/pages/publish/PublishStatusList.tsx b/Composer/packages/client/src/pages/publish/PublishStatusList.tsx index 40c7d98768..6de9ad8011 100644 --- a/Composer/packages/client/src/pages/publish/PublishStatusList.tsx +++ b/Composer/packages/client/src/pages/publish/PublishStatusList.tsx @@ -12,29 +12,18 @@ import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import moment from 'moment'; import { useState } from 'react'; import formatMessage from 'format-message'; +import { PublishResult } from '@botframework-composer/types'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { SharedColors } from '@uifabric/fluent-theme'; import { listRoot, tableView, detailList } from './styles'; export interface IStatusListProps { - items: IStatus[]; + items: PublishResult[]; + updateItems: (items: PublishResult[]) => void; isRollbackSupported: boolean; - onLogClick: (item: IStatus) => void; - onRollbackClick: (item: IStatus) => void; - updateItems: (items: IStatus[]) => void; -} - -export interface IStatus { - id: string; - time: string; - status: number; - message: string; - comment: string; - action?: { - href: string; - label: string; - }; + onLogClick: (item: PublishResult) => void; + onRollbackClick: (item: PublishResult) => void; } function onRenderDetailsHeader(props, defaultRender) { @@ -54,7 +43,7 @@ export const PublishStatusList: React.FC = (props) => { const sortByDate = (ev: React.MouseEvent, column: IColumn): void => { if (column.isSorted && items) { column.isSortedDescending = !column.isSortedDescending; - const newItems: IStatus[] = items.slice().reverse(); + const newItems: PublishResult[] = items.slice().reverse(); props.updateItems(newItems); } }; @@ -68,7 +57,7 @@ export const PublishStatusList: React.FC = (props) => { maxWidth: 90, isRowHeader: true, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return {moment(item.time).format('h:mm a')}; }, isPadded: true, @@ -83,7 +72,7 @@ export const PublishStatusList: React.FC = (props) => { isRowHeader: true, onColumnClick: sortByDate, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return {moment(item.time).format('MM-DD-YYYY')}; }, isPadded: true, @@ -96,7 +85,7 @@ export const PublishStatusList: React.FC = (props) => { minWidth: 40, maxWidth: 40, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { if (item.status === 200) { return ; } else if (item.status === 202) { @@ -121,7 +110,7 @@ export const PublishStatusList: React.FC = (props) => { isCollapsible: true, isMultiline: true, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return ( {item.message} @@ -151,7 +140,7 @@ export const PublishStatusList: React.FC = (props) => { isCollapsible: true, isMultiline: true, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return {item.comment}; }, isPadded: true, @@ -165,7 +154,7 @@ export const PublishStatusList: React.FC = (props) => { isCollapsible: true, isMultiline: true, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return ( = (props) => { isCollapsible: true, isMultiline: true, data: 'string', - onRender: (item: IStatus) => { + onRender: (item: PublishResult) => { return ( void; - current: PublishTarget | null; - targets: PublishTarget[]; - types: PublishType[]; - updateSettings: (name: string, type: string, configuration: string) => Promise; -} - -const CreatePublishTarget: React.FC = (props) => { - const { current } = props; - const [targetType, setTargetType] = useState(current?.type); - const [name, setName] = useState(current ? current.name : ''); - const [config, setConfig] = useState(current ? JSON.parse(current.configuration) : undefined); - const [errorMessage, setErrorMsg] = useState(''); - const [pluginConfigIsValid, setPluginConfigIsValid] = useState(false); - const userSettings = useRecoilValue(userSettingsState); - - const targetTypes = useMemo(() => { - return props.types.map((t) => ({ key: t.name, text: t.description })); - }, [props.targets]); - - const updateType = (_e, option?: IDropdownOption) => { - const type = props.types.find((t) => t.name === option?.key); - - if (type) { - setTargetType(type.name); - } - }; - - const updateConfig = (newConfig) => { - setConfig(newConfig); - }; - - const isNameValid = (newName) => { - if (!newName || newName.trim() === '') { - setErrorMsg(formatMessage('Must have a name')); - } else { - const exists = !!props.targets?.find((t) => t.name.toLowerCase() === newName?.toLowerCase()); - - if (exists) { - setErrorMsg(formatMessage('A profile with that name already exists.')); - } - } - }; - - const selectedTarget = useMemo(() => { - return props.types.find((t) => t.name === targetType); - }, [props.targets, targetType]); - - const targetBundleId = useMemo(() => { - return targetType ? props.types.find((t) => t.name === targetType)?.bundleId : undefined; - }, [props.targets, targetType]); - - const updateName = (e, newName) => { - setErrorMsg(''); - setName(newName); - isNameValid(newName); - }; - - const saveDisabled = useMemo(() => { - const disabled = !targetType || !name || !!errorMessage; - if (targetBundleId) { - // plugin config must also be valid - return disabled || !pluginConfigIsValid; - } - return disabled; - }, [errorMessage, name, pluginConfigIsValid, targetType]); - - // setup plugin APIs - useEffect(() => { - PluginAPI.publish.setPublishConfig = (config) => updateConfig(config); - PluginAPI.publish.setConfigIsValid = (valid) => setPluginConfigIsValid(valid); - PluginAPI.publish.useConfigBeingEdited = () => [current ? JSON.parse(current.configuration) : undefined]; - }, [current, targetType, name]); - - const submit = async (_e) => { - if (targetType) { - await props.updateSettings(name, targetType, JSON.stringify(config) || '{}'); - props.closeDialog(); - } - }; - - const publishTargetContent = useMemo(() => { - if (selectedTarget?.bundleId) { - let publishSurfaceStyles; - switch (selectedTarget.extensionId) { - case 'pva-publish-composer': - publishSurfaceStyles = pvaPublishSurface; - break; - - default: - publishSurfaceStyles = defaultPublishSurface; - break; - } - - // render custom plugin view - return ( -
- -
- ); - } - // render default instruction / schema editor view - return ( - - {selectedTarget?.instructions &&

{selectedTarget?.instructions}

} -
{formatMessage('Publish Configuration')}
- -