diff --git a/extensions/azurePublish/src/components/ChooseProvisionAction.tsx b/extensions/azurePublish/src/components/ChooseProvisionAction.tsx new file mode 100644 index 0000000000..448f5ceb8f --- /dev/null +++ b/extensions/azurePublish/src/components/ChooseProvisionAction.tsx @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import formatMessage from 'format-message'; +import styled from '@emotion/styled'; +import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; +import { Link } from 'office-ui-fabric-react/lib/Link'; + +// ---------- Styles ---------- // + +const Root = styled.div` + height: 100%; + width: 100%; + display: grid; + grid-template-columns: 30% 1fr; + grid-template-rows: 1fr; +`; + +const ChoicesPane = styled.div` + height: 100%; + width: 100%; +`; + +const ContentPane = styled(Stack)` + border-left: 1px solid ${NeutralColors.gray30}; + height: 100%; +`; + +const Content = styled(Stack)` + padding: 20px; +`; + +const Title = styled(Text)` + font-size: ${FluentTheme.fonts.xLarge.fontSize}; + margin: 8px 0; +`; + +const Summary = styled(Stack)` + margin: 8px 0; +`; + +const Details = styled(Stack)` + margin: 10px 0; +`; + +const Instruction = styled(Stack)` + margin: 10px 0; +`; + +const InstructionTitle = styled(Text)` + font-size: ${FluentTheme.fonts.smallPlus.fontSize}; + text-transform: uppercase; + margin: 8px 0; +`; + +const InstructionDetails = styled.div` + margin: 10px 0; +`; + +const ResourceTitle = styled(Stack)` + margin: 10px 0; +`; + +const LearnMoreLink = styled(Link)` + user-select: none; +`; + +// ---------- CreateActionContent ---------- // + +const CreateActionContent = () => { + return ( + + {formatMessage('Create New Resources')} + + + {formatMessage( + 'Select this option when you want to provision new Azure resources and publish a bot. A subscription to ' + )} + + {formatMessage('Microsoft Azure')} + {formatMessage(' isrequired')} + +
+ + {formatMessage('Step 1')} + + {formatMessage('Sign in to Azure.')} + + + + {formatMessage('Step 2')} + + + {formatMessage( + 'Select tenant & subscription, enter resource group name and resource name, and select region.' + )} + + + + + {formatMessage('Step 3')} + + + {formatMessage( + 'Review & create new resources. Once provisioned these resources will be available in your Azure portal.' + )} + + + +
+ + {formatMessage('Learn More')} + +
+ ); +}; + +const ImportActionContent = () => { + return ( + + {formatMessage('Import existing resources')} + +

+ {formatMessage('Select this option to import existing Azure resources and publish a bot.')} +

+

+ + {formatMessage( + 'Edit the JSON file in the Publish Configuration field. You will need to find the values of associated resources in your Azure portal. A list of required and optional resources may include:' + )} + +

+
+
+ + {formatMessage('Microsoft Application Registration')} + + + {formatMessage('Azure Hosting')} + + + {formatMessage('Microsoft Bot Channels Registration')} + + + {formatMessage('Azure Cosmos DB')} + + + {formatMessage('Application Insights')} + + + {formatMessage('Azure Blob Storage')} + + + {formatMessage('Microsoft Language Understanding (LUIS)')} + + + {formatMessage('Microsoft QnA Maker')} + +
+ + {formatMessage('Learn More')} + +
+ ); +}; + +const GenerateActionContent = () => { + return ( + + {formatMessage('Hand off to admin')} + + + {formatMessage( + 'Select this option to request your Azure admin to provision resources on your behalf, for example, when you don’t have proper permissions to use Azure or you want to generate resources from a sovereign cloud.' + )} + + +
+ + {formatMessage('Step 1')} + + + {formatMessage( + 'Add resources you need for the bot and generate a resource request to share with your Azure admin.' + )} + + + + + {formatMessage('Step 2')} + + + {formatMessage( + 'Once you get the resource details from your Azure admin, use them to import existing resources.' + )} + + + +
+ + {formatMessage('Learn More')} + +
+ ); +}; + +// ---------- Helpers ---------- // + +const choiceOptions: IChoiceGroupOption[] = [ + { key: 'create', text: 'Create new resources' }, + { key: 'import', text: 'Import existing resources' }, + { key: 'generate', text: 'Hand off to admin' }, +]; + +// ---------- ChooseProvisionActionStep ---------- // + +type ProvisionAction = 'create' | 'import' | 'generate'; + +type Props = { + /** + * The optional choice of provisioning action. + * Defaults to 'create'. + */ + choice?: ProvisionAction; + + onChoiceChanged: (choice: ProvisionAction) => void; +}; + +/** + * Provides the step where the user can choose a provisioning action. + */ +export const ChooseProvisionAction = ({ choice: controlledChoice, onChoiceChanged }) => { + const [choice, setChoice] = React.useState(controlledChoice || 'create'); + + React.useEffect(() => { + setChoice(controlledChoice || 'create'); + }, [controlledChoice]); + + React.useEffect(() => { + onChoiceChanged(choice); + }, [choice]); + + const renderContent = React.useMemo(() => { + switch (choice) { + case 'create': + return ; + case 'import': + return ; + case 'generate': + return ; + } + }, [choice]); + + return ( + + + { + setChoice(option.key); + }} + /> + + {renderContent} + + ); +}; diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx index 211d2f247a..19f13a6b39 100644 --- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx +++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx @@ -14,8 +14,6 @@ import { LoadingSpinner, ProvisionHandoff } from '@bfc/ui-shared'; import { ScrollablePane, ScrollbarVisibility, - ChoiceGroup, - IChoiceGroupOption, DetailsList, DetailsListLayoutMode, IColumn, @@ -47,6 +45,7 @@ import { import { ChooseResourcesList } from './ChooseResourcesList'; import { getExistResources, removePlaceholder, decodeToken, defaultExtensionState } from './util'; import { ResourceGroupPicker } from './ResourceGroupPicker'; +import { ChooseProvisionAction } from './ChooseProvisionAction'; type ProvisionFormData = { creationType: string; @@ -86,14 +85,8 @@ const iconStyle = (required) => { }; }; -const choiceOptions: IChoiceGroupOption[] = [ - { key: 'create', text: 'Create new Azure resources' }, - { key: 'import', text: 'Import existing Azure resources' }, - { key: 'generate', text: 'Generate resource request' }, -]; - const PageTypes = { - SelectTenant: 'tenant', + ChooseAction: 'chooseAction', ConfigProvision: 'config', AddResources: 'add', ReviewResource: 'review', @@ -101,6 +94,10 @@ const PageTypes = { }; const DialogTitle = { + CHOOSE_ACTION: { + title: formatMessage('Configure resources'), + subText: formatMessage('How you would like to provision Azure resources to your publishing profile?'), + }, CONFIG_RESOURCES: { title: formatMessage('Configure resources'), subText: formatMessage('How you would like to provision your Azure resources to publish your bot?'), @@ -293,7 +290,7 @@ export const AzureProvisionDialog: React.FC = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const [importConfig, setImportConfig] = useState(); - const [page, setPage] = useState(PageTypes.ConfigProvision); + const [page, setPage] = useState(PageTypes.ChooseAction); const [listItems, setListItems] = useState<(ResourcesItem & { icon?: string })[]>(); const [reviewListItems, setReviewListItems] = useState([]); const isMounted = useRef(); @@ -332,6 +329,27 @@ export const AzureProvisionDialog: React.FC = () => { setHandoffInstructions(instructions); }; + const setPageAndTitle = (page: string) => { + setPage(page); + switch (page) { + case PageTypes.AddResources: + setTitle(DialogTitle.ADD_RESOURCES); + break; + case PageTypes.ChooseAction: + setTitle(DialogTitle.CHOOSE_ACTION); + break; + case PageTypes.ConfigProvision: + setTitle(DialogTitle.CONFIG_RESOURCES); + break; + case PageTypes.EditJson: + setTitle(DialogTitle.EDIT); + break; + case PageTypes.ReviewResource: + setTitle(DialogTitle.REVIEW); + break; + } + }; + function updateFormData(field: K, value: ProvisionFormData[K]) { setFormData((current) => ({ ...current, [field]: value })); } @@ -372,7 +390,7 @@ export const AzureProvisionDialog: React.FC = () => { }; useEffect(() => { - setPage(PageTypes.ConfigProvision); + setPage(PageTypes.ChooseAction); // TODO: need to get the tenant id from the auth config when running as web app, // for electron we will always fetch tenants. if (userShouldProvideTokens()) { @@ -390,8 +408,7 @@ export const AzureProvisionDialog: React.FC = () => { expiration: (decoded.exp || 0) * 1000, // convert to ms, sessionExpired: false, }); - setPage(PageTypes.ConfigProvision); - setTitle(DialogTitle.CONFIG_RESOURCES); + setPageAndTitle(PageTypes.ChooseAction); setLoginErrorMsg(undefined); } } else { @@ -596,8 +613,7 @@ export const AzureProvisionDialog: React.FC = () => { const items = requireList.concat(optionalList); setListItems(items); - setPage(PageTypes.AddResources); - setTitle(DialogTitle.ADD_RESOURCES); + setPageAndTitle(PageTypes.AddResources); }, [extensionResourceOptions] ); @@ -659,126 +675,111 @@ export const AzureProvisionDialog: React.FC = () => { const isNewResourceGroupName = !resourceGroupNames.includes(formData.resourceGroup); + const PageChooseAction = ( + + { + updateFormData('creationType', choice); + }} + /> + + ); + const PageFormConfig = ( -
-
- { - updateFormData('creationType', option.key); - }} - /> -
-
- {formData.creationType === 'create' && ( -
- ({ 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} - /> - { - updateFormData('resourceGroup', choice.name); - setErrorResourceGroupName(choice.errorMessage); - }} - /> - - - { - updateFormData('luisLocation', o.key as string); - }} - /> - +
+ -
- {formatMessage('Publish Configuration')} -
-
+ 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} + /> + { + updateFormData('resourceGroup', choice.name); + setErrorResourceGroupName(choice.errorMessage); + }} + /> + -
+ 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); + }} + /> +
); useEffect(() => { if (listItems?.length === 0) { - setTitle(DialogTitle.EDIT); - setPage(PageTypes.EditJson); + setPageAndTitle(PageTypes.EditJson); } }, [listItems]); @@ -839,7 +840,7 @@ export const AzureProvisionDialog: React.FC = () => { ); const PageFooter = useMemo(() => { - if (page === PageTypes.ConfigProvision) { + if (page === PageTypes.ChooseAction) { return (
{currentUser ? ( @@ -871,6 +872,58 @@ export const AzureProvisionDialog: React.FC = () => { onBack(); }} /> + { + switch (formData.creationType) { + case 'import': + setPageAndTitle(PageTypes.EditJson); + break; + case 'create': + case 'generate': + default: + setPageAndTitle(PageTypes.ConfigProvision); + break; + } + }} + /> +
+ + ); + } else if (page === PageTypes.ConfigProvision) { + return ( +
+ {currentUser ? ( + + ) : ( +
{ + clearAll(); + closeDialog(); + logOut(); + }} + > + {formatMessage('Sign out')} +
+ )} +
+ { + clearAll(); + setItem(profileName, formData); + setPageAndTitle(PageTypes.ChooseAction); + }} + /> {formData.creationType === 'create' && ( { style={{ margin: '0 4px' }} text={formatMessage('Back')} onClick={() => { - setPage(PageTypes.ConfigProvision); - setTitle(DialogTitle.CONFIG_RESOURCES); + setPageAndTitle(PageTypes.ConfigProvision); }} /> { if (formData.creationType === 'generate') { setShowHandoff(true); } else { - setPage(PageTypes.ReviewResource); - setTitle(DialogTitle.REVIEW); + setPageAndTitle(PageTypes.ReviewResource); let selectedResources = formData.requiredResources.concat(formData.enabledResources); selectedResources = selectedResources.map((item) => { let region = currentConfig?.region || formData.region; @@ -964,8 +1015,7 @@ export const AzureProvisionDialog: React.FC = () => { style={{ margin: '0 4px' }} text={formatMessage('Back')} onClick={() => { - setPage(PageTypes.AddResources); - setTitle(DialogTitle.ADD_RESOURCES); + setPageAndTitle(PageTypes.AddResources); }} /> { />
+ {page === PageTypes.ChooseAction && PageChooseAction} {page === PageTypes.ConfigProvision && PageFormConfig} {page === PageTypes.AddResources && PageAddResources()} {page === PageTypes.ReviewResource && PageReview}