diff --git a/Composer/packages/client/__tests__/pages/botProjectsSettings/PublishTarget.test.tsx b/Composer/packages/client/__tests__/pages/botProjectsSettings/PublishTarget.test.tsx index 6b485c057f..c3d6f963fd 100644 --- a/Composer/packages/client/__tests__/pages/botProjectsSettings/PublishTarget.test.tsx +++ b/Composer/packages/client/__tests__/pages/botProjectsSettings/PublishTarget.test.tsx @@ -13,7 +13,7 @@ import { isShowAuthDialog } from '../../../src/utils/auth'; jest.mock('../../../src/utils/auth', () => ({ isShowAuthDialog: jest.fn(), getTokenFromCache: jest.fn(), - isGetTokenFromUser: jest.fn(), + userShouldProvideTokens: jest.fn(), })); const state = { diff --git a/Composer/packages/client/src/components/ManageLuis/ManageLuis.tsx b/Composer/packages/client/src/components/ManageLuis/ManageLuis.tsx index 313b7eeca6..f0d6857d05 100644 --- a/Composer/packages/client/src/components/ManageLuis/ManageLuis.tsx +++ b/Composer/packages/client/src/components/ManageLuis/ManageLuis.tsx @@ -23,7 +23,7 @@ import { ProvisionHandoff } from '@bfc/ui-shared'; import { AuthClient } from '../../utils/authClient'; import { AuthDialog } from '../../components/Auth/AuthDialog'; import { armScopes } from '../../constants'; -import { getTokenFromCache, isShowAuthDialog, isGetTokenFromUser } from '../../utils/auth'; +import { getTokenFromCache, isShowAuthDialog, userShouldProvideTokens } from '../../utils/auth'; import { LUIS_REGIONS } from '../../constants'; import { dispatcherState } from '../../recoilModel/atoms'; @@ -94,7 +94,7 @@ export const ManageLuis = (props: ManageLuisProps) => { const hasAuth = async () => { let newtoken = ''; - if (isGetTokenFromUser()) { + if (userShouldProvideTokens()) { if (isShowAuthDialog(false)) { setShowAuthDialog(true); } diff --git a/Composer/packages/client/src/components/ManageQNA/ManageQNA.tsx b/Composer/packages/client/src/components/ManageQNA/ManageQNA.tsx index 4780aadcac..42d99adf2a 100644 --- a/Composer/packages/client/src/components/ManageQNA/ManageQNA.tsx +++ b/Composer/packages/client/src/components/ManageQNA/ManageQNA.tsx @@ -26,7 +26,7 @@ import { ProvisionHandoff } from '@bfc/ui-shared'; import { AuthClient } from '../../utils/authClient'; import { AuthDialog } from '../../components/Auth/AuthDialog'; import { armScopes } from '../../constants'; -import { getTokenFromCache, isShowAuthDialog, isGetTokenFromUser } from '../../utils/auth'; +import { getTokenFromCache, isShowAuthDialog, userShouldProvideTokens } from '../../utils/auth'; import { dispatcherState } from '../../recoilModel/atoms'; type ManageQNAProps = { @@ -102,7 +102,7 @@ export const ManageQNA = (props: ManageQNAProps) => { const hasAuth = async () => { let newtoken = ''; - if (isGetTokenFromUser()) { + if (userShouldProvideTokens()) { if (isShowAuthDialog(false)) { setShowAuthDialog(true); } diff --git a/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx b/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx index 2c66d943f4..09deab0e7f 100644 --- a/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx +++ b/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx @@ -25,7 +25,7 @@ import { settingsState } from '../../../recoilModel'; import { AuthClient } from '../../../utils/authClient'; import { AuthDialog } from '../../../components/Auth/AuthDialog'; import { armScopes } from '../../../constants'; -import { getTokenFromCache, isShowAuthDialog, isGetTokenFromUser } from '../../../utils/auth'; +import { getTokenFromCache, isShowAuthDialog, userShouldProvideTokens } from '../../../utils/auth'; import httpClient from '../../../utils/httpUtil'; import { dispatcherState } from '../../../recoilModel'; import { @@ -119,7 +119,7 @@ export const ABSChannels: React.FC = (props) => { navigateTo(`/bot/${projectId}/botProjectsSettings/#addNewPublishProfile`); } else { let newtoken = ''; - if (isGetTokenFromUser()) { + if (userShouldProvideTokens()) { if (isShowAuthDialog(false)) { setShowAuthDialog(true); } @@ -315,7 +315,7 @@ export const ABSChannels: React.FC = (props) => { const hasAuth = async () => { let newtoken = ''; - if (isGetTokenFromUser()) { + if (userShouldProvideTokens()) { if (isShowAuthDialog(false)) { setShowAuthDialog(true); } 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 cfdf46df12..660548e8a0 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 @@ -10,7 +10,7 @@ import { Dialog } from 'office-ui-fabric-react/lib/Dialog'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { useRecoilValue } from 'recoil'; -import { getTokenFromCache, isGetTokenFromUser, setTenantId, getTenantIdFromCache } from '../../../utils/auth'; +import { getTokenFromCache, userShouldProvideTokens, setTenantId, getTenantIdFromCache } from '../../../utils/auth'; import { PublishType } from '../../../recoilModel/types'; import { PluginAPI } from '../../../plugins/api'; import { PluginHost } from '../../../components/PluginHost/PluginHost'; @@ -91,8 +91,12 @@ export const PublishProfileDialog: React.FC = (props) graphToken: getTokenFromCache('graphToken'), }; }; + /** @deprecated use `userShouldProvideTokens` instead */ PluginAPI.publish.isGetTokenFromUser = () => { - return isGetTokenFromUser(); + return userShouldProvideTokens(); + }; + PluginAPI.publish.userShouldProvideTokens = () => { + return userShouldProvideTokens(); }; PluginAPI.publish.setTitle = (value) => { setTitle(value); @@ -160,7 +164,7 @@ export const PublishProfileDialog: React.FC = (props) const fullConfig = { ...config, name: name, type: targetType }; let arm, graph; - if (!isGetTokenFromUser()) { + if (!userShouldProvideTokens()) { const tenantId = getTenantIdFromCache(); // require tenant id to be set by plugin (handles multiple tenant scenario) if (!tenantId) { diff --git a/Composer/packages/client/src/pages/publish/Publish.tsx b/Composer/packages/client/src/pages/publish/Publish.tsx index 90e0b4ef41..92b0485b92 100644 --- a/Composer/packages/client/src/pages/publish/Publish.tsx +++ b/Composer/packages/client/src/pages/publish/Publish.tsx @@ -17,7 +17,7 @@ import { getSensitiveProperties } from '../../recoilModel/dispatchers/utils/proj import { getTokenFromCache, isShowAuthDialog, - isGetTokenFromUser, + userShouldProvideTokens, setTenantId, getTenantIdFromCache, } from '../../utils/auth'; @@ -246,13 +246,13 @@ const Publish: React.FC string; savePublishConfig?: (config: PublishConfig) => void; getTokenFromCache?: () => { accessToken: string; graphToken: string }; + /** @deprecated use `userShouldProvideTokens` instead */ isGetTokenFromUser?: () => boolean; + userShouldProvideTokens?: () => boolean; getTenantIdFromCache?: () => string; setTenantId?: (value: string) => void; } @@ -62,6 +64,7 @@ class API implements IAPI { savePublishConfig: undefined, getTokenFromCache: undefined, isGetTokenFromUser: undefined, + userShouldProvideTokens: undefined, getTenantIdFromCache: undefined, setTenantId: undefined, }; diff --git a/Composer/packages/client/src/utils/auth.ts b/Composer/packages/client/src/utils/auth.ts index 70d36f4633..cafa4326bc 100644 --- a/Composer/packages/client/src/utils/auth.ts +++ b/Composer/packages/client/src/utils/auth.ts @@ -341,7 +341,7 @@ export function isShowAuthDialog(needGraph: boolean): boolean { } } -export function isGetTokenFromUser(): boolean { +export function userShouldProvideTokens(): boolean { if (isElectron()) { return false; } else if (authConfig.clientId && authConfig.redirectUrl && authConfig.tenantId) { diff --git a/Composer/packages/electron-server/__tests__/auth/oneAuthService.test.ts b/Composer/packages/electron-server/__tests__/auth/oneAuthService.test.ts index bf30f970ad..b1b6af6476 100644 --- a/Composer/packages/electron-server/__tests__/auth/oneAuthService.test.ts +++ b/Composer/packages/electron-server/__tests__/auth/oneAuthService.test.ts @@ -56,6 +56,7 @@ describe('OneAuth Serivce', () => { beforeEach(() => { jest.resetModules(); oneAuthService = new OneAuthInstance(); + // eslint-disable-next-line no-underscore-dangle (oneAuthService as any)._oneAuth = mockOneAuth; mockOneAuth.acquireCredentialInteractively.mockClear(); mockOneAuth.acquireCredentialSilently.mockClear(); @@ -169,38 +170,82 @@ describe('OneAuth Serivce', () => { const result = await service.getAccessToken({}); expect(result).toEqual({ accessToken: '', acquiredAt: 0, expiryTime: 99999999999 }); + // reset node env + process.env.NODE_ENV = 'test'; }); - it('should get a list of tenants', async () => { - const mockTenants = [ - { - tenantId: 'tenant1', - }, - { - tenantId: 'tenant2', - }, - { - tenantId: 'tenant3', - }, - ]; - mockFetch.mockResolvedValueOnce({ - json: jest.fn().mockResolvedValue({ value: mockTenants }), + describe('#getTenants', () => { + it('should get a list of tenants', async () => { + const mockTenants = [ + { + tenantId: 'tenant1', + }, + { + tenantId: 'tenant2', + }, + { + tenantId: 'tenant3', + }, + ]; + mockFetch.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ value: mockTenants }), + }); + const tenants = await oneAuthService.getTenants(); + + // it should have initialized + expect(mockOneAuth.setLogPiiEnabled).toHaveBeenCalled(); + expect(mockOneAuth.setLogCallback).toHaveBeenCalled(); + expect(mockOneAuth.initialize).toHaveBeenCalled(); + + // it should have signed in + expect(mockOneAuth.signInInteractively).toHaveBeenCalled(); + expect((oneAuthService as any).signedInARMAccount).toEqual(mockAccount); + + // it should have called the tenants API + expect(mockFetch).toHaveBeenCalledWith('https://management.azure.com/tenants?api-version=2020-01-01', { + headers: { + Authorization: 'Bearer someToken', + }, + }); + + expect(tenants).toBe(mockTenants); }); - const tenants = await oneAuthService.getTenants(); - // it should have initialized - expect(mockOneAuth.setLogPiiEnabled).toHaveBeenCalled(); - expect(mockOneAuth.setLogCallback).toHaveBeenCalled(); - expect(mockOneAuth.initialize).toHaveBeenCalled(); - - // it should have signed in - expect(mockOneAuth.signInInteractively).toHaveBeenCalled(); - expect((oneAuthService as any).signedInARMAccount).toEqual(mockAccount); - - // it should have called the tenants API - expect(mockFetch.mock.calls[0][0]).toBe('https://management.azure.com/tenants?api-version=2020-01-01'); - - expect(tenants).toBe(mockTenants); + it('should not attempt to sign in if token already fetched', async () => { + const mockTenants = [ + { + tenantId: 'tenant1', + }, + { + tenantId: 'tenant2', + }, + { + tenantId: 'tenant3', + }, + ]; + mockFetch.mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ value: mockTenants }), + }); + (oneAuthService as any).signedInARMAccount = { some: 'account' }; + (oneAuthService as any).tenantToken = 'cached-token'; + const tenants = await oneAuthService.getTenants(); + + // it should have initialized + expect(mockOneAuth.setLogPiiEnabled).toHaveBeenCalled(); + expect(mockOneAuth.setLogCallback).toHaveBeenCalled(); + expect(mockOneAuth.initialize).toHaveBeenCalled(); + + expect(mockOneAuth.signInInteractively).not.toHaveBeenCalled(); + + // it should have called the tenants API + expect(mockFetch).toHaveBeenCalledWith('https://management.azure.com/tenants?api-version=2020-01-01', { + headers: { + Authorization: 'Bearer cached-token', + }, + }); + + expect(tenants).toBe(mockTenants); + }); }); it('should throw an error if something goes wrong while getting a list of tenants', async () => { diff --git a/Composer/packages/electron-server/__tests__/setupTests.js b/Composer/packages/electron-server/__tests__/setupTests.js index 0c94be19e4..c0a45f199d 100644 --- a/Composer/packages/electron-server/__tests__/setupTests.js +++ b/Composer/packages/electron-server/__tests__/setupTests.js @@ -1,4 +1,5 @@ -process.env.DEBUG = 'composer*'; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. jest.mock('electron', () => ({ app: { diff --git a/Composer/packages/electron-server/src/auth/oneAuthService.ts b/Composer/packages/electron-server/src/auth/oneAuthService.ts index 125fac6c51..48d1565634 100644 --- a/Composer/packages/electron-server/src/auth/oneAuthService.ts +++ b/Composer/packages/electron-server/src/auth/oneAuthService.ts @@ -62,6 +62,8 @@ export class OneAuthInstance extends OneAuthBase { private _oneAuth: typeof OneAuth | null = null; //eslint-disable-line private signedInAccount: OneAuth.Account | undefined; private signedInARMAccount: OneAuth.Account | undefined; + /** Token solely used to fetch tenants */ + private tenantToken: string | undefined; constructor() { super(); @@ -189,17 +191,19 @@ export class OneAuthInstance extends OneAuthBase { if (!this.initialized) { this.initialize(); } - // log the user into the infrastructure tenant to get a token that can be used on the "tenants" API - log('Logging user into ARM...'); - this.signedInARMAccount = undefined; - const signInParams = new this.oneAuth.AuthParameters(DEFAULT_AUTH_SCHEME, ARM_AUTHORITY, ARM_RESOURCE, '', ''); - const result: OneAuth.AuthResult = await this.oneAuth.signInInteractively('', signInParams, ''); - this.signedInARMAccount = result.account; - const token = result.credential.value; + + if (!this.signedInARMAccount || !this.tenantToken) { + // log the user into the infrastructure tenant to get a token that can be used on the "tenants" API + log('Logging user into ARM...'); + const signInParams = new this.oneAuth.AuthParameters(DEFAULT_AUTH_SCHEME, ARM_AUTHORITY, ARM_RESOURCE, '', ''); + const result: OneAuth.AuthResult = await this.oneAuth.signInInteractively('', signInParams, ''); + this.signedInARMAccount = result.account; + this.tenantToken = result.credential.value; + } // call the tenants API const tenantsResult = await fetch('https://management.azure.com/tenants?api-version=2020-01-01', { - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${this.tenantToken}` }, }); const tenants = (await tenantsResult.json()) as GetTenantsResult; log('Got Azure tenants for user: %O', tenants.value); diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 962d4ed871..235fe31c13 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -181,10 +181,6 @@ async function main(show = false) { const mainWindow = ElectronWindow.getInstance().browserWindow; initAppMenu(mainWindow); if (mainWindow) { - if (process.env.COMPOSER_DEV_TOOLS) { - mainWindow.webContents.openDevTools(); - } - await mainWindow.loadURL(getBaseUrl()); if (show) { @@ -296,6 +292,10 @@ async function run() { const mainWindow = getMainWindow(); mainWindow?.webContents.send('session-update', 'session-started'); + + if (process.env.COMPOSER_DEV_TOOLS) { + mainWindow?.webContents.openDevTools(); + } }); // Quit when all windows are closed. diff --git a/Composer/packages/extension-client/src/hooks/usePublishApi.ts b/Composer/packages/extension-client/src/hooks/usePublishApi.ts index 7603091498..dbf6661efa 100644 --- a/Composer/packages/extension-client/src/hooks/usePublishApi.ts +++ b/Composer/packages/extension-client/src/hooks/usePublishApi.ts @@ -55,8 +55,12 @@ export function usePublishApi() { function setTenantId(value): void { window[ComposerGlobalName].setTenantId(value); } + function userShouldProvideTokens(): boolean { + return window[ComposerGlobalName].userShouldProvideTokens(); + } + /** @deprecated use `userShouldProvideTokens` instead */ function isGetTokenFromUser(): boolean { - return window[ComposerGlobalName].isGetTokenFromUser(); + return window[ComposerGlobalName].userShouldProvideTokens(); } return { publishConfig: getPublishConfig(), @@ -71,6 +75,7 @@ export function usePublishApi() { savePublishConfig, getTokenFromCache, isGetTokenFromUser, + userShouldProvideTokens, getTenantIdFromCache, setTenantId, }; diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 0896e2620d..ad01e7e3a4 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -4025,4 +4025,4 @@ "your_template_requires_qna_maker_to_access_content_a4ca6f76": { "message": "Your template requires QnA Maker to access content for your bot." } -} +} \ No newline at end of file diff --git a/extensions/azurePublish/src/components/ResourceGroupPicker.tsx b/extensions/azurePublish/src/components/ResourceGroupPicker.tsx index b41cc7374a..aafe5163bf 100644 --- a/extensions/azurePublish/src/components/ResourceGroupPicker.tsx +++ b/extensions/azurePublish/src/components/ResourceGroupPicker.tsx @@ -46,6 +46,7 @@ type ResourceGroupItemChoice = { }; type Props = { + disabled: boolean; /** * The resource groups to choose from. * Set to undefined to disable this picker. @@ -93,6 +94,7 @@ const onRenderLabel = (props) => { }; export const ResourceGroupPicker = ({ + disabled, resourceGroupNames, selectedResourceGroupName: controlledSelectedName, newResourceGroupName: controlledNewName, @@ -169,7 +171,7 @@ export const ResourceGroupPicker = ({ ariaLabel={formatMessage( 'A resource group is a collection of resources that share the same lifecycle, permissions, and policies' )} - disabled={loading} + disabled={loading || disabled} label={formatMessage('Resource group')} options={options} placeholder={formatMessage('Select one')} diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx index 38f87d32c2..30afb1bcd7 100644 --- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx +++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx @@ -3,15 +3,14 @@ import * as React from 'react'; import formatMessage from 'format-message'; import styled from '@emotion/styled'; -import { useState, useMemo, useEffect, Fragment, useCallback, useRef, Suspense } from 'react'; +import { useState, useMemo, useEffect, Fragment, useCallback, useRef } 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, useLocalStorage } from '@bfc/extension-client'; import { Subscription } from '@azure/arm-subscriptions/esm/models'; import { DeployLocation, AzureTenant } from '@botframework-composer/types'; import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; -import { LoadingSpinner } from '@bfc/ui-shared'; -import { ProvisionHandoff } from '@bfc/ui-shared'; +import { LoadingSpinner, ProvisionHandoff } from '@bfc/ui-shared'; import { ScrollablePane, ScrollbarVisibility, @@ -23,7 +22,6 @@ import { TooltipHost, Icon, TextField, - Spinner, Persona, IPersonaProps, PersonaSize, @@ -50,6 +48,18 @@ import { ChooseResourcesList } from './ChooseResourcesList'; import { getExistResources, removePlaceholder, decodeToken, defaultExtensionState } from './util'; import { ResourceGroupPicker } from './ResourceGroupPicker'; +type ProvisionFormData = { + creationType: string; + tenantId: string; + subscriptionId: string; + resourceGroup: string; + hostname: string; + region: string; + luisLocation: string; + enabledResources: ResourcesItem[]; + requiredResources: ResourcesItem[]; +}; + // ---------- Styles ---------- // const AddResourcesSectionName = styled(Text)` @@ -211,6 +221,28 @@ const reviewCols: IColumn[] = [ }, ]; +const getHostname = (config) => { + if (config?.hostname) { + return config.hostname; + } else if (config?.name) { + return config?.environment ? `${config.name}-${config.environment}` : config.name; + } +}; + +const getDefaultFormData = (currentProfile, defaults) => { + return { + creationType: defaults.creationType ?? 'create', + tenantId: currentProfile?.tenantId, + subscriptionId: currentProfile?.subscriptionId ?? defaults.subscriptionId, + resourceGroup: currentProfile?.resourceGroup ?? defaults.resourceGroup, + hostname: getHostname(currentProfile) ?? defaults.hostname, + region: currentProfile?.region ?? defaults.region, + luisLocation: currentProfile?.settings?.luis?.region ?? defaults.luisLocation, + enabledResources: defaults.enabledResources ?? [], + requiredResources: defaults.requireResources ?? [], + }; +}; + export const AzureProvisionDialog: React.FC = () => { const { currentProjectId, @@ -224,7 +256,7 @@ export const AzureProvisionDialog: React.FC = () => { getType, getName, getTokenFromCache, - isGetTokenFromUser, + userShouldProvideTokens, getTenantIdFromCache, setTenantId, } = usePublishApi(); @@ -236,36 +268,30 @@ export const AzureProvisionDialog: React.FC = () => { const currentConfig = removePlaceholder(publishConfig); const extensionState = { ...defaultExtensionState, ...getItem(profileName) }; + const [token, setToken] = useState(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [currentUser, setCurrentUser] = useState(undefined); + + // form options + const [allTenants, setAllTenants] = useState([]); const [subscriptions, setSubscriptions] = useState(); const [subscriptionsErrorMessage, setSubscriptionsErrorMessage] = useState(); const [deployLocations, setDeployLocations] = useState([]); const [luisLocations, setLuisLocations] = useState([]); + const [extensionResourceOptions, setExtensionResourceOptions] = useState([]); - const [allTenants, setAllTenants] = useState([]); - const [selectedTenant, setSelectedTenant] = useState(); - const [token, setToken] = useState(); - const [currentUser, setCurrentUser] = useState(undefined); - const [loginErrorMsg, setLoginErrorMsg] = useState(''); + const [formData, setFormData] = useState(getDefaultFormData(currentConfig, extensionState)); - const [choice, setChoice] = useState(extensionState.choice); - const [currentSubscription, setCurrentSubscription] = useState(extensionState.subscriptionId); + // null = loading + const [loginErrorMsg, setLoginErrorMsg] = useState(''); const [resourceGroups, setResourceGroups] = useState(); const [isNewResourceGroupName, setIsNewResourceGroupName] = useState(true); - const [currentResourceGroupName, setCurrentResourceGroupName] = useState(extensionState.resourceGroup); const [errorResourceGroupName, setErrorResourceGroupName] = useState(); - - const [currentHostName, setHostName] = useState(extensionState.hostName); const [errorHostName, setErrorHostName] = useState(''); - 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(extensionState.enabledResources); // create from optional list - const [requireResources, setRequireResources] = useState(extensionState.requiredResources); const [isEditorError, setEditorError] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [importConfig, setImportConfig] = useState(); const [page, setPage] = useState(PageTypes.ConfigProvision); @@ -286,11 +312,11 @@ export const AzureProvisionDialog: React.FC = () => { const createQnAResource = resources.filter((r) => r.key === 'qna').length > 0; const provisionComposer = `node provisionComposer.js --subscriptionId ${ - currentSubscription ?? '' - } --name ${currentHostName ?? ''} + formData.subscriptionId ?? '' + } --name ${formData.hostname ?? ''} --appPassword=<16 CHAR PASSWORD> - --location=${currentLocation || 'westus'} - --resourceGroup=${currentResourceGroup || ''} + --location=${formData.region || 'westus'} + --resourceGroup=${formData.resourceGroup || ''} --createLuisResource=${createLuisResource} --createLuisAuthoringResource=${createLuisAuthoringResource} --createCosmosDb=${createCosmosDb} @@ -307,10 +333,14 @@ export const AzureProvisionDialog: React.FC = () => { setHandoffInstructions(instructions); }; + function updateFormData(field: K, value: ProvisionFormData[K]) { + setFormData((current) => ({ ...current, [field]: value })); + } + useEffect(() => { - const selectedResources = requireResources.concat(enabledResources); + const selectedResources = formData.requiredResources.concat(formData.enabledResources); updateHandoffInstructions(selectedResources); - }, [enabledResources]); + }, [formData.enabledResources]); useEffect(() => { isMounted.current = true; @@ -333,8 +363,6 @@ export const AzureProvisionDialog: React.FC = () => { expiration: (decoded.exp || 0) * 1000, // convert to ms, sessionExpired: false, }); - setPage(PageTypes.ConfigProvision); - setTitle(DialogTitle.CONFIG_RESOURCES); setLoginErrorMsg(undefined); }) .catch((err) => { @@ -345,15 +373,17 @@ export const AzureProvisionDialog: React.FC = () => { }; useEffect(() => { + setPage(PageTypes.ConfigProvision); // TODO: need to get the tenant id from the auth config when running as web app, // for electron we will always fetch tenants. - if (isGetTokenFromUser()) { + if (userShouldProvideTokens()) { const { accessToken } = getTokenFromCache(); setToken(accessToken); // decode token const decoded = decodeToken(accessToken); if (decoded) { + updateFormData('tenantId', decoded.tid); setCurrentUser({ token: accessToken, email: decoded.upn, @@ -364,51 +394,38 @@ export const AzureProvisionDialog: React.FC = () => { } } else { getTenants().then((tenants) => { - setAllTenants(tenants); - if (!getTenantIdFromCache()) { - if (isMounted.current && tenants?.length > 0) { - // if there is only 1 tenant, go ahead and fetch the token and store it in the cache - if (tenants.length === 1) { - setSelectedTenant(tenants[0].tenantId); - // getTokenForTenant(tenants[0].tenantId); - } else { - // seed tenant selection with first tenant - setSelectedTenant(tenants[0].tenantId); + if (isMounted.current) { + setAllTenants(tenants); + if (!getTenantIdFromCache()) { + if (tenants?.length > 0) { + // seed tenant selection with 1st tenant + updateFormData('tenantId', tenants[0].tenantId); } + } else { + updateFormData('tenantId', getTenantIdFromCache()); } - } else { - setSelectedTenant(getTenantIdFromCache()); } }); } }, []); useEffect(() => { - if (selectedTenant) { - getTokenForTenant(selectedTenant); - } - }, [selectedTenant]); - - useEffect(() => { - if (currentConfig) { - if (currentConfig.tennantId) { - setSelectedTenant(currentConfig.tennantId); - } - if (currentConfig.subscriptionId) { - setCurrentSubscription(currentConfig.subscriptionId); - } - if (currentConfig.resourceGroup) { - setCurrentResourceGroupName(currentConfig.resourceGroup); - } - if (currentConfig.hostname) { - setHostName(currentConfig.hostname); - } else if (currentConfig.name) { - setHostName( - currentConfig.environment ? `${currentConfig.name}-${currentConfig.environment}` : currentConfig.name - ); + if (formData.tenantId) { + if (formData.tenantId !== currentConfig?.tenantId) { + // reset form data when tenant id changes + setFormData((current) => ({ + ...current, + subscriptionId: '', + resourceGroup: '', + hostname: '', + region: '', + luisLocation: '', + })); } + + getTokenForTenant(formData.tenantId); } - }, [currentConfig]); + }, [formData.tenantId]); const getResources = async () => { try { @@ -418,6 +435,7 @@ export const AzureProvisionDialog: React.FC = () => { } } catch (err) { // todo: how do we handle API errors in this component + // eslint-disable-next-line no-console console.log('ERROR', err); } }; @@ -449,17 +467,18 @@ export const AzureProvisionDialog: React.FC = () => { }, [token]); const loadResourceGroups = async () => { - if (token && currentSubscription) { + if (token && formData.subscriptionId) { try { - const resourceGroups = await getResourceGroups(token, currentSubscription); + const resourceGroups = await getResourceGroups(token, formData.subscriptionId); if (isMounted.current) { setResourceGroups(resourceGroups); // After the resource groups load, isNewResourceGroupName can be determined - setIsNewResourceGroupName(!resourceGroups?.some((r) => r.name === currentResourceGroupName)); + setIsNewResourceGroupName(!resourceGroups?.some((r) => r.name === formData.resourceGroup)); } } catch (err) { // todo: how do we handle API errors in this component + // eslint-disable-next-line no-console console.log('ERROR', err); if (isMounted.current) { setResourceGroups(undefined); @@ -472,19 +491,19 @@ export const AzureProvisionDialog: React.FC = () => { useEffect(() => { loadResourceGroups(); - }, [token, currentSubscription]); + }, [token, formData.subscriptionId]); const subscriptionOptions = useMemo(() => { return subscriptions?.map((t) => ({ key: t.subscriptionId, text: t.displayName })); }, [subscriptions]); const deployLocationsOption = useMemo((): IDropdownOption[] => { - return deployLocations.map((t) => ({ key: t.name, text: t.displayName })); - }, [deployLocations]); + return (token && deployLocations?.map((t) => ({ key: t.name, text: t.displayName }))) || []; + }, [token, deployLocations]); const luisLocationsOption = useMemo((): IDropdownOption[] => { - return luisLocations.map((t) => ({ key: t.name, text: t.displayName })); - }, [luisLocations]); + return (token && luisLocations?.map((t) => ({ key: t.name, text: t.displayName }))) || []; + }, [token, luisLocations]); const checkNameAvailability = useCallback( (newName: string) => { @@ -492,9 +511,9 @@ export const AzureProvisionDialog: React.FC = () => { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { - if (currentSubscription && publishType === 'azurePublish') { + if (formData.subscriptionId && publishType === 'azurePublish') { // check app name whether exist or not - CheckWebAppNameAvailability(token, newName, currentSubscription).then((value) => { + CheckWebAppNameAvailability(token, newName, formData.subscriptionId).then((value) => { if (isMounted.current) { if (!value.nameAvailable) { setErrorHostName(value.message); @@ -506,48 +525,38 @@ export const AzureProvisionDialog: React.FC = () => { } }, 500); }, - [publishType, currentSubscription, token] + [publishType, formData.subscriptionId, token] ); const newHostName = useCallback( (e, newName) => { - setHostName(newName); + updateFormData('hostname', newName); // debounce name check checkNameAvailability(newName); }, [checkNameAvailability] ); - const updateCurrentLocation = useMemo( - () => (_e, option?: IDropdownOption) => { + const updateCurrentLocation = useCallback( + (_e, option?: IDropdownOption) => { const location = deployLocations.find((t) => t.name === option?.key); if (location) { - setLocation(location.name); + updateFormData('region', location.name); const region = luisLocations.find((item) => item.name === location.name); if (region) { - setCurrentLuisLocation(region.name); + updateFormData('luisLocation', region.name); } else { - setCurrentLuisLocation(luisLocations[0].name); + updateFormData('luisLocation', luisLocations[0].name); } } }, [deployLocations, luisLocations] ); - const updateLuisLocation = useMemo( - () => (_e, option?: IDropdownOption) => { - const location = luisLocations.find((t) => t.name === option?.key); - if (location) { - setCurrentLuisLocation(location.name); - } - }, - [luisLocations] - ); - useEffect(() => { - if (currentSubscription && token) { + if (formData.subscriptionId && token) { // get resource group under subscription - getDeployLocations(token, currentSubscription).then((data: DeployLocation[]) => { + getDeployLocations(token, formData.subscriptionId).then((data: DeployLocation[]) => { if (isMounted.current) { setDeployLocations(data); const luRegions = getLuisAuthoringRegions(); @@ -556,10 +565,10 @@ export const AzureProvisionDialog: React.FC = () => { } }); } - }, [currentSubscription, token]); + }, [formData.subscriptionId, token]); - const onNext = useMemo( - () => (hostname) => { + const onNext = useCallback( + (hostname) => { // get resources already have const alreadyHave = getExistResources(currentConfig); @@ -579,9 +588,10 @@ export const AzureProvisionDialog: React.FC = () => { // set review list const requireList = result.filter((item) => item.required); - setRequireResources(requireList); const optionalList = result.filter((item) => !item.required); - setEnabledResources(optionalList); + updateFormData('requiredResources', requireList); + updateFormData('enabledResources', optionalList); + const items = requireList.concat(optionalList); setListItems(items); @@ -598,14 +608,11 @@ export const AzureProvisionDialog: React.FC = () => { closeDialog(); }, []); - const onSave = useMemo( - () => () => { - savePublishConfig(importConfig); - clearAll(); - closeDialog(); - }, - [importConfig] - ); + const onSave = useCallback(() => { + savePublishConfig(importConfig); + clearAll(); + closeDialog(); + }, [importConfig]); const onRenderSecondaryText = useMemo( () => (props: IPersonaProps) => { @@ -625,27 +632,27 @@ export const AzureProvisionDialog: React.FC = () => { ); const isNextDisabled = useMemo(() => { - return ( - !currentSubscription || - !currentResourceGroupName || - !currentHostName || - !currentLocation || - subscriptionsErrorMessage || - errorResourceGroupName || - errorHostName !== '' + return Boolean( + !formData.subscriptionId || + !formData.resourceGroup || + !formData.hostname || + !formData.region || + subscriptionsErrorMessage || + errorResourceGroupName || + errorHostName !== '' ); }, [ - currentSubscription, - currentResourceGroupName, - currentHostName, - currentLocation, + formData.subscriptionId, + formData.resourceGroup, + formData.hostname, + formData.region, errorResourceGroupName, errorHostName, ]); const isSelectAddResources = useMemo(() => { - return enabledResources.length > 0 || requireResources.length > 0; - }, [enabledResources]); + return formData.enabledResources.length > 0 || formData.requiredResources.length > 0; + }, [formData.enabledResources]); const resourceGroupNames = resourceGroups?.map((r) => r.name) || []; @@ -659,140 +666,108 @@ export const AzureProvisionDialog: React.FC = () => {
{ - setChoice(option); + selectedKey={formData.creationType || 'create'} + onChange={(_e, option) => { + updateFormData('creationType', option.key); }} />
- }> - {choice.key === 'create' && ( -
- ({ key: t.tenantId, text: t.displayName }))} - selectedKey={selectedTenant} - styles={{ root: { paddingBottom: '8px' } }} - onChange={(_e, o) => { - setSelectedTenant(o.key as string); - }} - onRenderLabel={onRenderLabel} - /> - { - setCurrentSubscription(o.key as string); - }} - onRenderLabel={onRenderLabel} - /> - { - setIsNewResourceGroupName(choice.isNew); - setCurrentResourceGroupName(choice.name); - setErrorResourceGroupName(choice.errorMessage); - }} - /> - - {currentConfig?.region ? ( - - ) : ( - + {formData.creationType === 'create' && ( + + - )} - {!currentConfig?.settings?.luis?.region && currentLocation !== currentLuisLocation && ( - + disabled={allTenants.length === 1 || currentConfig?.tenantId} + errorMessage={loginErrorMsg} + label={formatMessage('Azure Directory')} + options={allTenants.map((t) => ({ key: t.tenantId, text: t.displayName }))} + selectedKey={formData.tenantId} + styles={{ root: { paddingBottom: '8px' } }} + onChange={(_e, o) => { + updateFormData('tenantId', o.key as string); + }} + onRenderLabel={onRenderLabel} + /> + { + updateFormData('subscriptionId', o.key as string); + }} + onRenderLabel={onRenderLabel} + /> + { + setIsNewResourceGroupName(choice.isNew); + updateFormData('resourceGroup', choice.name); + setErrorResourceGroupName(choice.errorMessage); + }} + /> + - )} - {choice.key === 'import' && ( -
-
- {formatMessage('Publish Configuration')} -
- { - setEditorError(false); - setImportConfig(value); - }} - onError={() => { - setEditorError(true); - }} - /> + disabled={currentConfig?.hostname || currentConfig?.name} + errorMessage={errorHostName} + label={formatMessage('Resource name')} + placeholder={formatMessage('Name of your services')} + styles={{ root: { paddingBottom: '8px' } }} + value={formData.hostname} + onChange={newHostName} + onRenderLabel={onRenderLabel} + /> + + { + updateFormData('luisLocation', o.key as string); + }} + /> + + )} + {formData.creationType === 'import' && ( +
+
+ {formatMessage('Publish Configuration')}
- )} - +
+ )}
@@ -809,7 +784,7 @@ export const AzureProvisionDialog: React.FC = () => { if (listItems) { const requiredListItems = listItems.filter((item) => item.required); const optionalListItems = listItems.filter((item) => !item.required); - const selectedResourceKeys = enabledResources.map((r) => r.key); + const selectedResourceKeys = formData.enabledResources.map((r) => r.key); return ( { selectedKeys={selectedResourceKeys} onSelectionChanged={(keys) => { const newSelection = optionalListItems.filter((item) => keys.includes(item.key)); - setEnabledResources(newSelection); + updateFormData('enabledResources', newSelection); }} /> @@ -890,37 +865,28 @@ export const AzureProvisionDialog: React.FC = () => { text={formatMessage('Back')} onClick={() => { clearAll(); - setItem(profileName, { - subscriptionId: currentSubscription, - resourceGroup: currentResourceGroupName, - hostName: currentHostName, - location: currentLocation, - luisLocation: currentLuisLocation, - enabledResources: enabledResources, - requiredResources: requireResources, - choice: choice, - }); + setItem(profileName, formData); onBack(); }} /> - {choice.key === 'create' && ( + {formData.creationType === 'create' && ( { - onNext(currentHostName); + onNext(formData.hostname); }} /> )} - {choice.key === 'generate' && ( + {formData.creationType === 'generate' && ( onNext(currentHostName)} + onClick={() => onNext(formData.hostname)} /> )} - {choice.key === 'import' && ( + {formData.creationType === 'import' && ( { style={{ margin: '0 4px' }} text={formatMessage('Next')} onClick={() => { - if (choice.key === 'generate') { + if (formData.creationType === 'generate') { setShowHandoff(true); } else { setPage(PageTypes.ReviewResource); setTitle(DialogTitle.REVIEW); - let selectedResources = requireResources.concat(enabledResources); + let selectedResources = formData.requiredResources.concat(formData.enabledResources); selectedResources = selectedResources.map((item) => { - let region = currentConfig?.region || currentLocation; + let region = currentConfig?.region || formData.region; if (item.key.includes('luis')) { - region = currentLuisLocation; + region = formData.luisLocation; } return { ...item, region: region, - resourceGroup: currentConfig?.resourceGroup || currentResourceGroupName, + resourceGroup: currentConfig?.resourceGroup || formData.resourceGroup, }; }); setReviewListItems(selectedResources); @@ -1005,13 +971,13 @@ export const AzureProvisionDialog: React.FC = () => { style={{ margin: '0 4px' }} text={formatMessage('Done')} onClick={() => { - const selectedResources = requireResources.concat(enabledResources); + const selectedResources = formData.requiredResources.concat(formData.enabledResources); onSubmit({ - subscription: currentSubscription, - resourceGroup: currentResourceGroupName, - hostname: currentHostName, - location: currentLocation, - luisLocation: currentLuisLocation || currentLocation, + subscription: formData.subscriptionId, + resourceGroup: formData.resourceGroup, + hostname: formData.hostname, + location: formData.region, + luisLocation: formData.luisLocation || formData.region, type: publishType, externalResources: selectedResources, }); @@ -1039,24 +1005,7 @@ export const AzureProvisionDialog: React.FC = () => { ); } - }, [ - onSave, - page, - choice, - isEditorError, - isNextDisabled, - currentSubscription, - currentResourceGroupName, - currentHostName, - currentLocation, - publishType, - extensionResourceOptions, - currentUser, - enabledResources, - requireResources, - currentLuisLocation, - selectedTenant, - ]); + }, [onSave, page, formData, isEditorError, isNextDisabled, publishType, extensionResourceOptions, currentUser]); // if we haven't loaded the token yet, show a loading spinner // unless we need to select the tenant first diff --git a/extensions/azurePublish/src/components/util.ts b/extensions/azurePublish/src/components/util.ts index 5a24b93ffc..f6a5869b42 100644 --- a/extensions/azurePublish/src/components/util.ts +++ b/extensions/azurePublish/src/components/util.ts @@ -64,10 +64,10 @@ export const getExistResources = (config) => { export const defaultExtensionState = { subscriptionId: '', resourceGroup: '', - hostName: '', - location: '', + hostname: '', + region: '', luisLocation: '', enabledResources: [], requiredResources: [], - choice: { key: 'create', text: 'Create new Azure resources' }, + creationType: 'create', }; diff --git a/extensions/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json b/extensions/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json index 8eb2a51950..9bac519926 100644 --- a/extensions/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json +++ b/extensions/samples/assets/shared/scripts/DeploymentTemplates/qna-template.json @@ -1,221 +1,217 @@ { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "name": { - "type": "string", - "defaultValue": "[resourceGroup().name]" - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "[resourceGroup().name]", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "appInsightsName": { - "type": "string", - "defaultValue": "[resourceGroup().name]" - }, - "appInsightsLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "qnaMakerServiceName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-qna')]" - }, - "qnaMakerServiceSku": { - "type": "string", - "defaultValue": "S0" - }, - "qnaMakerServiceLocation": { - "type": "string", - "defaultValue": "westus" - }, - "qnaMakerSearchName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-search')]" - }, - "qnaMakerSearchSku": { - "type": "string", - "defaultValue": "standard" - }, - "qnaMakerSearchLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "qnaMakerWebAppName": { - "type": "string", - "defaultValue": "[concat(parameters('name'), '-qnahost')]" - }, - "qnaMakerWebAppLocation": { - "type": "string", - "defaultValue": "[resourceGroup().location]" + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "The name of the new App Service Plan." } }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "qnaMakerSearchName": "[toLower(replace(parameters('qnaMakerSearchName'), '_', ''))]", - "qnaMakerWebAppName": "[replace(parameters('qnaMakerWebAppName'), '_', '')]" - }, - "resources": [ - { - "apiVersion": "2018-02-01", - "name": "1d41002f-62a1-49f3-bd43-2f3f32a19cbb", - "type": "Microsoft.Resources/deployments", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [] - } - } - }, - { - "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "condition": "[not(variables('useExistingAppServicePlan'))]", - "name": "[variables('servicePlanName')]", - "apiVersion": "2018-02-01", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "properties": { - "name": "[variables('servicePlanName')]" + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "appInsightsName": { + "type": "string", + "defaultValue": "[resourceGroup().name]" + }, + "appInsightsLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "qnaMakerServiceName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-qna')]" + }, + "qnaMakerServiceSku": { + "type": "string", + "defaultValue": "S0" + }, + "qnaMakerServiceLocation": { + "type": "string", + "defaultValue": "westus" + }, + "qnaMakerSearchName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-search')]" + }, + "qnaMakerSearchSku": { + "type": "string", + "defaultValue": "standard" + }, + "qnaMakerSearchLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "qnaMakerWebAppName": { + "type": "string", + "defaultValue": "[concat(parameters('name'), '-qnahost')]" + }, + "qnaMakerWebAppLocation": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "qnaMakerSearchName": "[toLower(replace(parameters('qnaMakerSearchName'), '_', ''))]", + "qnaMakerWebAppName": "[replace(parameters('qnaMakerWebAppName'), '_', '')]" + }, + "resources": [ + { + "apiVersion": "2018-02-01", + "name": "1d41002f-62a1-49f3-bd43-2f3f32a19cbb", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] } - }, - { - "comments": "app insights", - "type": "Microsoft.Insights/components", - "kind": "web", - "apiVersion": "2015-05-01", - "name": "[parameters('appInsightsName')]", - "location": "[parameters('appInsightsLocation')]", - "properties": { - "Application_Type": "web" + } + }, + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "app insights", + "type": "Microsoft.Insights/components", + "kind": "web", + "apiVersion": "2015-05-01", + "name": "[parameters('appInsightsName')]", + "location": "[parameters('appInsightsLocation')]", + "properties": { + "Application_Type": "web" + } + }, + { + "comments": "Cognitive service key for all QnA Maker knowledgebases.", + "type": "Microsoft.CognitiveServices/accounts", + "kind": "QnAMaker", + "apiVersion": "2017-04-18", + "name": "[parameters('qnaMakerServiceName')]", + "location": "[parameters('qnaMakerServiceLocation')]", + "sku": { + "name": "[parameters('qnaMakerServiceSku')]" + }, + "properties": { + "apiProperties": { + "qnaRuntimeEndpoint": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]" } }, - { - "comments": "Cognitive service key for all QnA Maker knowledgebases.", - "type": "Microsoft.CognitiveServices/accounts", - "kind": "QnAMaker", - "apiVersion": "2017-04-18", - "name": "[parameters('qnaMakerServiceName')]", - "location": "[parameters('qnaMakerServiceLocation')]", - "sku": { - "name": "[parameters('qnaMakerServiceSku')]" - }, - "properties": { - "apiProperties": { - "qnaRuntimeEndpoint": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]" + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", + "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]", + "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]" + ] + }, + { + "comments": "Search service for QnA Maker service.", + "type": "Microsoft.Search/searchServices", + "apiVersion": "2015-08-19", + "name": "[variables('qnaMakerSearchName')]", + "location": "[parameters('qnaMakerSearchLocation')]", + "sku": { + "name": "[parameters('qnaMakerSearchSku')]" + }, + "properties": { + "replicaCount": 1, + "partitionCount": 1, + "hostingMode": "default" + } + }, + { + "comments": "Web app for QnA Maker service.", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('qnaMakerWebAppName')]", + "location": "[parameters('qnaMakerWebAppLocation')]", + "properties": { + "enabled": true, + "name": "[variables('qnaMakerWebAppName')]", + "hostingEnvironment": "", + "serverFarmId": "[concat('/subscriptions/', Subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('servicePlanName'))]", + "siteConfig": { + "cors": { + "allowedOrigins": ["*"] } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", - "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]", - "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]" - ] - }, - { - "comments": "Search service for QnA Maker service.", - "type": "Microsoft.Search/searchServices", - "apiVersion": "2015-08-19", - "name": "[variables('qnaMakerSearchName')]", - "location": "[parameters('qnaMakerSearchLocation')]", - "sku": { - "name": "[parameters('qnaMakerSearchSku')]" - }, - "properties": { - "replicaCount": 1, - "partitionCount": 1, - "hostingMode": "default" } }, - { - "comments": "Web app for QnA Maker service.", - "type": "Microsoft.Web/sites", - "apiVersion": "2016-08-01", - "name": "[variables('qnaMakerWebAppName')]", - "location": "[parameters('qnaMakerWebAppLocation')]", - "properties": { - "enabled": true, - "name": "[variables('qnaMakerWebAppName')]", - "hostingEnvironment": "", - "serverFarmId": "[concat('/subscriptions/', Subscription().SubscriptionId,'/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('servicePlanName'))]", - "siteConfig": { - "cors": { - "allowedOrigins": [ - "*" - ] - } + "dependsOn": ["[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]"], + "resources": [ + { + "apiVersion": "2016-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", + "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]", + "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]" + ], + "properties": { + "AzureSearchName": "[variables('qnaMakerSearchName')]", + "AzureSearchAdminKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName')), '2015-08-19').primaryKey]", + "UserAppInsightsKey": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]", + "UserAppInsightsName": "[parameters('appInsightsName')]", + "UserAppInsightsAppId": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').AppId]", + "PrimaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-PrimaryEndpointKey')]", + "SecondaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-SecondaryEndpointKey')]", + "DefaultAnswer": "No good match found in KB.", + "EnableMultipleTestIndex": "true", + "QNAMAKER_EXTENSION_VERSION": "latest" } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "resources": [ - { - "apiVersion": "2016-08-01", - "name": "appsettings", - "type": "config", - "dependsOn": [ - "[resourceId('Microsoft.Web/Sites', variables('qnaMakerWebAppName'))]", - "[resourceId('Microsoft.Insights/components', parameters('appInsightsName'))]", - "[resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName'))]" - ], - "properties": { - "AzureSearchName": "[variables('qnaMakerSearchName')]", - "AzureSearchAdminKey": "[listAdminKeys(resourceId('Microsoft.Search/searchServices/', variables('qnaMakerSearchName')), '2015-08-19').primaryKey]", - "UserAppInsightsKey": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]", - "UserAppInsightsName": "[parameters('appInsightsName')]", - "UserAppInsightsAppId": "[reference(resourceId('Microsoft.Insights/components/', parameters('appInsightsName')), '2015-05-01').AppId]", - "PrimaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-PrimaryEndpointKey')]", - "SecondaryEndpointKey": "[concat(variables('qnaMakerWebAppName'), '-SecondaryEndpointKey')]", - "DefaultAnswer": "No good match found in KB.", - "EnableMultipleTestIndex": "true", - "QNAMAKER_EXTENSION_VERSION": "latest" - } - } - ] - } - ], - "outputs": { - "qna": { - "type": "object", - "value": { - "endpoint": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]", - "subscriptionKey": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('qnaMakerServiceName')),'2017-04-18').key1]" } + ] + } + ], + "outputs": { + "qna": { + "type": "object", + "value": { + "endpoint": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('qnaMakerWebAppName'))).hostNames[0])]", + "subscriptionKey": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', parameters('qnaMakerServiceName')),'2017-04-18').key1]" } } } +}