diff --git a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx index fe9edda001..b3c228a774 100644 --- a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx +++ b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx @@ -45,6 +45,7 @@ interface PluginHostProps { async function attachPluginAPI( win: Window, id: string, + bundleId: string, type: PluginType, shell?: object, settings?: ExtensionSettings @@ -56,6 +57,7 @@ async function attachPluginAPI( } win.Composer.__extensionId = id; + win.Composer.__bundleId = bundleId; win.Composer.__pluginType = type; win.Composer.settings = settings ?? {}; win.Composer.sync(shell); @@ -96,7 +98,7 @@ export const PluginHost: React.FC = (props) => { const iframeWindow = targetRef.current?.contentWindow as Window; const iframeDocument = targetRef.current?.contentDocument as Document; - await attachPluginAPI(iframeWindow, name, type, shell, extensionSettings); + await attachPluginAPI(iframeWindow, name, bundle, type, shell, extensionSettings); //load the bundle for the specified plugin const pluginScriptId = `plugin-${type}-${name}`; diff --git a/Composer/packages/client/src/pages/botProject/create-publish-profile/ProfileFormDialog.tsx b/Composer/packages/client/src/pages/botProject/create-publish-profile/ProfileFormDialog.tsx index 78e6135711..0844fc1c33 100644 --- a/Composer/packages/client/src/pages/botProject/create-publish-profile/ProfileFormDialog.tsx +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/ProfileFormDialog.tsx @@ -6,8 +6,7 @@ import { jsx, css } from '@emotion/core'; import formatMessage from 'format-message'; import { SharedColors } from '@uifabric/fluent-theme'; import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; -import { useState, useMemo, useCallback, Fragment, useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useState, useMemo, useCallback, Fragment } from 'react'; 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'; @@ -17,21 +16,18 @@ import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { separator } from '../../publish/styles'; -import { graphScopes } from '../../../constants'; import { PublishType } from '../../../recoilModel/types'; -import { PluginAPI } from '../../../plugins/api'; -import { dispatcherState } from '../../../recoilModel'; -import { AuthClient } from '../../../utils/authClient'; -import { getTenantIdFromCache, setTenantId, getTokenFromCache, isGetTokenFromUser } from '../../../utils/auth'; type ProfileFormDialogProps = { onDismiss: () => void; targets: PublishTarget[]; types: PublishType[]; onNext: () => void; - updateSettings: (name: string, type: string, configuration: string) => Promise; - projectId: string; setType: (value) => void; + name: string; + targetType: string; + setName: (value: string) => void; + setTargetType: (value: string) => void; current?: { index: number; item: PublishTarget } | null; }; const labelContainer = css` @@ -72,11 +68,8 @@ const onRenderLabel = (props) => { }; export const ProfileFormDialog: React.FC = (props) => { - const { onDismiss, targets, types, onNext, updateSettings, projectId, setType, current } = props; - const [name, setName] = useState(current?.item.name || ''); + const { name, setName, targetType, setTargetType, onDismiss, targets, types, onNext, setType, current } = props; const [errorMessage, setErrorMsg] = useState(''); - const [targetType, setTargetType] = useState(current?.item.type || ''); - const { provisionToTarget } = useRecoilValue(dispatcherState); const updateName = (e, newName) => { setName(newName); @@ -116,42 +109,6 @@ export const ProfileFormDialog: React.FC = (props) => { 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 - let tenantId = getTenantIdFromCache(); - if (!tenantId) { - const tenants = await AuthClient.getTenants(); - tenantId = tenants?.[0]?.tenantId; - setTenantId(tenantId); - } - arm = await AuthClient.getARMTokenForTenant(tenantId); - graph = await AuthClient.getAccessToken(graphScopes); - } else { - // get token from cache - arm = getTokenFromCache('accessToken'); - graph = getTokenFromCache('graphToken'); - } - provisionToTarget(fullConfig, config.type, projectId, arm, graph, current?.item); - }; - }, [name, targetType]); - return ( 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 index 32da95ffdb..ee38c09061 100644 --- a/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx @@ -38,6 +38,9 @@ const Page = { export const PublishProfileDialog: React.FC = (props) => { const { current, types, projectId, closeDialog, targets, setPublishTargets } = props; + const [name, setName] = useState(current?.item.name || ''); + const [targetType, setTargetType] = useState(current?.item.type || ''); + const [page, setPage] = useState(Page.ProfileForm); const [publishSurfaceStyles, setStyles] = useState(defaultPublishSurface); const { provisionToTarget } = useRecoilValue(dispatcherState); @@ -140,38 +143,39 @@ export const PublishProfileDialog: React.FC = (props) ); useEffect(() => { - if (current?.item?.type) { - PluginAPI.publish.getType = () => { - return current?.item?.type; - }; - PluginAPI.publish.getSchema = () => { - return types.find((t) => t.name === current?.item?.type)?.schema; - }; - PluginAPI.publish.savePublishConfig = (config) => { - savePublishTarget(current?.item.name, current?.item?.type, JSON.stringify(config) || '{}'); - }; - PluginAPI.publish.startProvision = async (config) => { - const fullConfig = { ...config, name: current.item.name, type: current.item.type }; - let arm, graph; - if (!isGetTokenFromUser()) { - // login or get token implicit - let tenantId = getTenantIdFromCache(); - if (!tenantId) { - const tenants = await AuthClient.getTenants(); - tenantId = tenants?.[0]?.tenantId; - setTenantId(tenantId); - } - arm = await AuthClient.getARMTokenForTenant(tenantId); - graph = await AuthClient.getAccessToken(graphScopes); - } else { - // get token from cache - arm = getTokenFromCache('accessToken'); - graph = getTokenFromCache('graphToken'); + PluginAPI.publish.getType = () => { + return targetType; + }; + PluginAPI.publish.getName = () => { + return name; + }; + PluginAPI.publish.getSchema = () => { + return types.find((t) => t.name === targetType)?.schema; + }; + PluginAPI.publish.savePublishConfig = (config) => { + savePublishTarget(name, targetType, JSON.stringify(config) || '{}'); + }; + PluginAPI.publish.startProvision = async (config) => { + const fullConfig = { ...config, name: name, type: targetType }; + let arm, graph; + if (!isGetTokenFromUser()) { + // login or get token implicit + let tenantId = getTenantIdFromCache(); + if (!tenantId) { + const tenants = await AuthClient.getTenants(); + tenantId = tenants?.[0]?.tenantId; + setTenantId(tenantId); } - provisionToTarget(fullConfig, config.type, projectId, arm, graph, current?.item); - }; - } - }, [current, types, savePublishTarget]); + arm = await AuthClient.getARMTokenForTenant(tenantId); + graph = await AuthClient.getAccessToken(graphScopes); + } else { + // get token from cache + arm = getTokenFromCache('accessToken'); + graph = getTokenFromCache('graphToken'); + } + provisionToTarget(fullConfig, config.type, projectId, arm, graph, current?.item); + }; + }, [name, targetType, types, savePublishTarget]); return ( @@ -197,11 +201,13 @@ export const PublishProfileDialog: React.FC = (props) { setPage(Page.ConfigProvision); diff --git a/Composer/packages/client/src/plugin-host-preload.ts b/Composer/packages/client/src/plugin-host-preload.ts index d2ab3d4c9a..7365c817b1 100644 --- a/Composer/packages/client/src/plugin-host-preload.ts +++ b/Composer/packages/client/src/plugin-host-preload.ts @@ -51,6 +51,7 @@ window.CodeEditors = CodeEditors; window.UIShared = UIShared; window.Composer = { __extensionId: '', + __bundleId: '', __pluginType: '', render: (component: React.ReactElement) => { ReactDOM.render(component, document.getElementById('root')); diff --git a/Composer/packages/client/src/plugins/api.ts b/Composer/packages/client/src/plugins/api.ts index 66c09b0e69..839fdb6ffc 100644 --- a/Composer/packages/client/src/plugins/api.ts +++ b/Composer/packages/client/src/plugins/api.ts @@ -32,6 +32,7 @@ interface PublishAPI { setTitle?: (value) => void; getSchema?: () => any; getType?: () => string; + getName?: () => string; savePublishConfig?: (config: PublishConfig) => void; getTokenFromCache?: () => { accessToken: string; graphToken: string }; isGetTokenFromUser?: () => boolean; diff --git a/Composer/packages/extension-client/src/hooks/index.ts b/Composer/packages/extension-client/src/hooks/index.ts index 9ff8373ee8..6681a49b0d 100644 --- a/Composer/packages/extension-client/src/hooks/index.ts +++ b/Composer/packages/extension-client/src/hooks/index.ts @@ -17,3 +17,4 @@ export * from './useLgApi'; export * from './useProjectApi'; export * from './usePublishApi'; export * from './useTelemetryClient'; +export * from './useLocalStorage'; diff --git a/Composer/packages/extension-client/src/hooks/useLocalStorage.ts b/Composer/packages/extension-client/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000000..cb5d0dfcdd --- /dev/null +++ b/Composer/packages/extension-client/src/hooks/useLocalStorage.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-underscore-dangle */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useCallback } from 'react'; + +const KEY = 'composer:extensions'; + +export function useLocalStorage() { + const { __extensionId, __bundleId } = window.Composer; + + const getExtensionState = useCallback(() => { + const allStatesString = window.localStorage.getItem(KEY); + const allStates = allStatesString ? JSON.parse(allStatesString) : {}; + const extensionState = allStates?.[__extensionId] || {}; + return extensionState; + }, [__extensionId]); + + const updateExtensionState = useCallback( + (value = undefined) => { + const allStatesString = window.localStorage.getItem(KEY); + const allStates = allStatesString ? JSON.parse(allStatesString) : {}; + const extensionState = allStates?.[__extensionId] || {}; + if (value) { + extensionState[__bundleId] = value; + } else { + delete extensionState[__bundleId]; + } + allStates[__extensionId] = extensionState; + window.localStorage.setItem(KEY, JSON.stringify(allStates)); + }, + [__extensionId, __bundleId] + ); + + const getAll = useCallback(() => { + return getExtensionState()?.[__bundleId]; + }, [getExtensionState, __bundleId]); + + const getItem = useCallback( + (key: string) => { + return getAll()?.[key]; + }, + [getAll] + ); + + const setItem = useCallback( + (key: string, value: any) => { + const bundleState = getAll() || {}; + bundleState[key] = value; + updateExtensionState(bundleState); + }, + [getAll] + ); + + const replaceAll = useCallback( + (value: any) => { + updateExtensionState(value); + }, + [updateExtensionState] + ); + + const clearAll = useCallback(() => { + updateExtensionState(); + }, [updateExtensionState]); + return { getItem, setItem, getAll, clearAll, replaceAll }; +} diff --git a/Composer/packages/extension-client/src/hooks/usePublishApi.ts b/Composer/packages/extension-client/src/hooks/usePublishApi.ts index 4b578aab51..7603091498 100644 --- a/Composer/packages/extension-client/src/hooks/usePublishApi.ts +++ b/Composer/packages/extension-client/src/hooks/usePublishApi.ts @@ -40,6 +40,9 @@ export function usePublishApi() { function getType(): string { return window[ComposerGlobalName].getType(); } + function getName(): string { + return window[ComposerGlobalName].getName(); + } function savePublishConfig(config): void { return window[ComposerGlobalName].savePublishConfig(config); } @@ -55,7 +58,6 @@ export function usePublishApi() { function isGetTokenFromUser(): boolean { return window[ComposerGlobalName].isGetTokenFromUser(); } - return { publishConfig: getPublishConfig(), startProvision, @@ -65,6 +67,7 @@ export function usePublishApi() { setTitle, getSchema, getType, + getName, savePublishConfig, getTokenFromCache, isGetTokenFromUser, diff --git a/Composer/packages/extension-client/src/types/window.d.ts b/Composer/packages/extension-client/src/types/window.d.ts index 281ae4ae4b..6b0c61c376 100644 --- a/Composer/packages/extension-client/src/types/window.d.ts +++ b/Composer/packages/extension-client/src/types/window.d.ts @@ -22,6 +22,7 @@ declare global { */ Composer: { __extensionId: string; + __bundleId: string; __pluginType: string; render: (component: React.ReactElement) => void; sync: (shell: Shell) => void; diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx index 93c4eeb7cf..19f8235e65 100644 --- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx +++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useState, useMemo, useEffect, Fragment, useCallback, useRef, Suspense } from 'react'; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button'; -import { logOut, usePublishApi, getTenants, getARMTokenForTenant } from '@bfc/extension-client'; +import { logOut, usePublishApi, getTenants, getARMTokenForTenant, useLocalStorage } from '@bfc/extension-client'; import { Subscription } from '@azure/arm-subscriptions/esm/models'; import { DeployLocation } from '@botframework-composer/types'; import { NeutralColors } from '@uifabric/fluent-theme'; @@ -37,13 +37,12 @@ import { AzureResourceTypes, ResourcesItem } from '../types'; import { getResourceList, getSubscriptions, - getResourceGroups, getDeployLocations, getPreview, getLuisAuthoringRegions, CheckWebAppNameAvailability, } from './api'; -import { getExistResources, removePlaceholder, decodeToken } from './util'; +import { getExistResources, removePlaceholder, decodeToken, defaultExtensionState } from './util'; const iconStyle = (required) => { return { @@ -248,14 +247,19 @@ export const AzureProvisionDialog: React.FC = () => { setTitle, getSchema, getType, + getName, getTokenFromCache, isGetTokenFromUser, getTenantIdFromCache, setTenantId, } = usePublishApi(); + + const { setItem, getItem, clearAll } = useLocalStorage(); // set type of publish - azurePublish or azureFunctionsPublish const publishType = getType(); + const profileName = getName(); const currentConfig = removePlaceholder(publishConfig); + const extensionState = { ...defaultExtensionState, ...getItem(profileName) }; const [subscriptions, setSubscriptions] = useState(); const [deployLocations, setDeployLocations] = useState([]); @@ -265,17 +269,19 @@ export const AzureProvisionDialog: React.FC = () => { const [currentUser, setCurrentUser] = useState(undefined); const [loginErrorMsg, setLoginErrorMsg] = useState(''); - const [choice, setChoice] = useState(choiceOptions[0]); - const [currentSubscription, setSubscription] = useState(''); - const [currentResourceGroup, setResourceGroup] = useState(''); - const [currentHostName, setHostName] = useState(''); + const [choice, setChoice] = useState(extensionState.choice); + const [currentSubscription, setSubscription] = useState(extensionState.subscriptionId); + const [currentResourceGroup, setResourceGroup] = useState(extensionState.resourceGroup); + const [currentHostName, setHostName] = useState(extensionState.hostName); const [errorHostName, setErrorHostName] = useState(''); const [errorResourceGroupName, setErrorResourceGroupName] = useState(''); - const [currentLocation, setLocation] = useState(currentConfig?.region); - const [currentLuisLocation, setCurrentLuisLocation] = useState(currentConfig?.settings?.luis?.region); + const [currentLocation, setLocation] = useState(currentConfig?.region || extensionState.location); + const [currentLuisLocation, setCurrentLuisLocation] = useState( + currentConfig?.settings?.luis?.region || extensionState.luisLocation + ); const [extensionResourceOptions, setExtensionResourceOptions] = useState([]); - const [enabledResources, setEnabledResources] = useState([]); // create from optional list - const [requireResources, setRequireResources] = useState([]); + const [enabledResources, setEnabledResources] = useState(extensionState.enabledResources); // create from optional list + const [requireResources, setRequireResources] = useState(extensionState.requiredResources); const [isEditorError, setEditorError] = useState(false); const [importConfig, setImportConfig] = useState(); @@ -342,7 +348,6 @@ export const AzureProvisionDialog: React.FC = () => { }); }) .catch((err) => { - console.log(err); setCurrentUser(undefined); setLoginErrorMsg(err.message || err.toString()); }); @@ -543,9 +548,10 @@ export const AzureProvisionDialog: React.FC = () => { ); const onSubmit = useMemo( - () => async (options) => { + () => (options) => { // call back to the main Composer API to begin this process... startProvision(options); + clearAll(); closeDialog(); }, [] @@ -554,6 +560,7 @@ export const AzureProvisionDialog: React.FC = () => { const onSave = useMemo( () => () => { savePublishConfig(importConfig); + clearAll(); closeDialog(); }, [importConfig] @@ -599,7 +606,7 @@ export const AzureProvisionDialog: React.FC = () => { const PageFormConfig = ( - + }> {subscriptionOption?.length > 0 && choice.key === 'create' && (
@@ -798,6 +805,7 @@ export const AzureProvisionDialog: React.FC = () => {
{ + clearAll(); closeDialog(); logOut(); }} @@ -806,7 +814,24 @@ export const AzureProvisionDialog: React.FC = () => {
)}
- + { + clearAll(); + setItem(profileName, { + subscriptionId: currentSubscription, + resourceGroup: currentResourceGroup, + hostName: currentHostName, + location: currentLocation, + luisLocation: currentLuisLocation, + enabledResources: enabledResources, + requiredResources: requireResources, + choice: choice, + }); + onBack(); + }} + /> {choice.key === 'create' ? ( { return result; } else return []; }; + +export const defaultExtensionState = { + subscriptionId: '', + resourceGroup: '', + hostName: '', + location: '', + luisLocation: '', + enabledResources: [], + requiredResources: [], + choice: { key: 'create', text: 'Create new Azure resources' }, +};