diff --git a/extensions/azurePublish/src/components/ResourceGroupPicker.tsx b/extensions/azurePublish/src/components/ResourceGroupPicker.tsx
new file mode 100644
index 0000000000..b41cc7374a
--- /dev/null
+++ b/extensions/azurePublish/src/components/ResourceGroupPicker.tsx
@@ -0,0 +1,204 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import * as React from 'react';
+import formatMessage from 'format-message';
+import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
+import { Icon } from 'office-ui-fabric-react/lib/Icon';
+import { Stack } from 'office-ui-fabric-react/lib/Stack';
+import { TextField } from 'office-ui-fabric-react/lib/TextField';
+import { FluentTheme } from '@uifabric/fluent-theme';
+import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
+
+import { useDebounce } from './useDebounce';
+
+const stackStyles = { root: { marginBottom: '8px' } };
+const dropdownStyles = { root: { marginBottom: '8px' }, dropdown: { width: '100%' } };
+const itemIconStyles = { marginRight: '8px' };
+const newNameTextFileStyles = { root: { marginTop: '10px' } };
+
+const getInfoIconStyle = (required) => {
+ return {
+ root: {
+ selectors: {
+ '&::before': {
+ content: required ? " '*'" : '',
+ color: FluentTheme.palette.red,
+ paddingRight: 3,
+ },
+ },
+ },
+ };
+};
+
+const CREATE_NEW_KEY = 'CREATE_NEW';
+
+const createNewOption: IDropdownOption = {
+ key: CREATE_NEW_KEY,
+ data: { iconName: 'Add' },
+ text: formatMessage('Create new'),
+};
+
+type ResourceGroupItemChoice = {
+ name: string;
+ isNew: boolean;
+ errorMessage?: string;
+};
+
+type Props = {
+ /**
+ * The resource groups to choose from.
+ * Set to undefined to disable this picker.
+ */
+ resourceGroupNames?: string[];
+ /**
+ * The selected name of the existing resource.
+ * When undefined, the 'Create new' option will be selected.
+ */
+ selectedResourceGroupName?: string;
+ /**
+ * The name chosen for a new resource group.
+ * Used when the 'Create new' option is selected.
+ */
+ newResourceGroupName?: string;
+ /**
+ * Called when the selection or new resource name changes.
+ */
+ onChange: (choice: ResourceGroupItemChoice) => void;
+};
+
+const onRenderLabel = (props) => {
+ return (
+
+
+ {` ${props.label} `}
+
+
+
+
+
+ );
+};
+
+export const ResourceGroupPicker = ({
+ resourceGroupNames,
+ selectedResourceGroupName: controlledSelectedName,
+ newResourceGroupName: controlledNewName,
+ onChange,
+}: Props) => {
+ // ----- Hooks -----//
+ const [selectedName, setSelectedName] = React.useState(controlledSelectedName || CREATE_NEW_KEY);
+ const [newName, setNewName] = React.useState(controlledNewName);
+ const debouncedNewName = useDebounce(newName, 300);
+ const [newNameErrorMessage, setNewNameErrorMessage] = React.useState('');
+
+ React.useEffect(() => {
+ setSelectedName(controlledSelectedName || CREATE_NEW_KEY);
+ }, [controlledSelectedName]);
+
+ React.useEffect(() => {
+ setNewName(controlledNewName || '');
+ }, [controlledNewName]);
+
+ React.useEffect(() => {
+ const alreadyExists = resourceGroupNames?.some((name) => name === debouncedNewName);
+
+ if (debouncedNewName && !debouncedNewName.match(/^[-\w._()]+$/)) {
+ setNewNameErrorMessage(
+ formatMessage(
+ 'Resource group names only allow alphanumeric characters, periods, underscores, hyphens and parenthesis and cannot end in a period.'
+ )
+ );
+ } else if (alreadyExists) {
+ setNewNameErrorMessage(formatMessage('A resource with this name already exists.'));
+ } else {
+ setNewNameErrorMessage(undefined);
+ }
+ }, [debouncedNewName, resourceGroupNames]);
+
+ React.useEffect(() => {
+ const isNew = selectedName === CREATE_NEW_KEY;
+ onChange({
+ isNew,
+ name: isNew ? debouncedNewName : selectedName,
+ errorMessage: isNew ? newNameErrorMessage : undefined,
+ });
+ }, [selectedName, debouncedNewName, newNameErrorMessage]);
+
+ const options = React.useMemo(() => {
+ const optionsList: IDropdownOption[] =
+ resourceGroupNames?.map((p) => {
+ return { key: p, text: p };
+ }) || [];
+
+ optionsList.unshift(createNewOption);
+ return optionsList;
+ }, [resourceGroupNames]);
+
+ // ----- Render -----//
+
+ const loading = resourceGroupNames === undefined;
+
+ const onRenderOption = (option) => {
+ return (
+
+ {option.data?.iconName && (
+
+ )}
+ {option.text}
+
+ );
+ };
+
+ return (
+
+ {
+ setSelectedName(opt.key as string);
+ }}
+ onRenderLabel={onRenderLabel}
+ onRenderOption={onRenderOption}
+ />
+ {selectedName === CREATE_NEW_KEY && (
+ {
+ setNewName(val || '');
+ }}
+ onRenderLabel={onRenderLabel}
+ />
+ )}
+
+ );
+};
diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx
index 0e2b6e7023..091f5dc715 100644
--- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx
+++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx
@@ -32,6 +32,7 @@ import {
} from 'office-ui-fabric-react';
import { JsonEditor } from '@bfc/code-editor';
import { SharedColors } from '@uifabric/fluent-theme';
+import { ResourceGroup } from '@azure/arm-resources/esm/models';
import { AzureResourceTypes, ResourcesItem } from '../types';
@@ -42,9 +43,11 @@ import {
getPreview,
getLuisAuthoringRegions,
CheckWebAppNameAvailability,
+ getResourceGroups,
} from './api';
import { ChooseResourcesList } from './ChooseResourcesList';
import { getExistResources, removePlaceholder, decodeToken, defaultExtensionState } from './util';
+import { ResourceGroupPicker } from './ResourceGroupPicker';
// ---------- Styles ---------- //
@@ -236,11 +239,15 @@ export const AzureProvisionDialog: React.FC = () => {
const [loginErrorMsg, setLoginErrorMsg] = useState('');
const [choice, setChoice] = useState(extensionState.choice);
- const [currentSubscription, setSubscription] = useState(extensionState.subscriptionId);
- const [currentResourceGroup, setResourceGroup] = useState(extensionState.resourceGroup);
+ const [currentSubscription, setCurrentSubscription] = useState(extensionState.subscriptionId);
+
+ 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 [errorResourceGroupName, setErrorResourceGroupName] = useState('');
const [currentLocation, setLocation] = useState(currentConfig?.region || extensionState.location);
const [currentLuisLocation, setCurrentLuisLocation] = useState(
currentConfig?.settings?.luis?.region || extensionState.luisLocation
@@ -252,7 +259,7 @@ export const AzureProvisionDialog: React.FC = () => {
const [isEditorError, setEditorError] = useState(false);
const [importConfig, setImportConfig] = useState();
- const [page, setPage] = useState();
+ const [page, setPage] = useState(PageTypes.ConfigProvision);
const [listItems, setListItems] = useState<(ResourcesItem & { icon?: string })[]>();
const [reviewListItems, setReviewListItems] = useState([]);
const isMounted = useRef();
@@ -342,10 +349,10 @@ export const AzureProvisionDialog: React.FC = () => {
setSelectedTenant(currentConfig.tennantId);
}
if (currentConfig.subscriptionId) {
- setSubscription(currentConfig.subscriptionId);
+ setCurrentSubscription(currentConfig.subscriptionId);
}
if (currentConfig.resourceGroup) {
- setResourceGroup(currentConfig.resourceGroup);
+ setCurrentResourceGroupName(currentConfig.resourceGroup);
}
if (currentConfig.hostname) {
setHostName(currentConfig.hostname);
@@ -378,6 +385,28 @@ export const AzureProvisionDialog: React.FC = () => {
}
}, [token]);
+ const loadResourceGroups = async () => {
+ if (token && currentSubscription) {
+ try {
+ const resourceGroups = await getResourceGroups(token, currentSubscription);
+ setResourceGroups(resourceGroups);
+
+ // After the resource groups load, isNewResourceGroupName can be determined
+ setIsNewResourceGroupName(!resourceGroups?.some((r) => r.name === currentResourceGroupName));
+ } catch (err) {
+ // todo: how do we handle API errors in this component
+ console.log('ERROR', err);
+ setResourceGroups(undefined);
+ }
+ } else {
+ setResourceGroups(undefined);
+ }
+ };
+
+ useEffect(() => {
+ loadResourceGroups();
+ }, [token, currentSubscription]);
+
const subscriptionOption = useMemo(() => {
return subscriptions?.map((t) => ({ key: t.subscriptionId, text: t.displayName }));
}, [subscriptions]);
@@ -413,25 +442,6 @@ export const AzureProvisionDialog: React.FC = () => {
[publishType, currentSubscription, token]
);
- const checkResourceGroupName = useCallback((group: string) => {
- if (group.match(/^[-\w._()]+$/)) {
- setErrorResourceGroupName('');
- } else {
- setErrorResourceGroupName(
- 'Resource group names only allow alphanumeric characters, periods, underscores, hyphens and parenthesis and cannot end in a period.'
- );
- }
- }, []);
-
- const updateCurrentResourceGroup = useMemo(
- () => (e, newGroup) => {
- setResourceGroup(newGroup);
- // check resource group name
- checkResourceGroupName(newGroup);
- },
- [checkResourceGroupName]
- );
-
const newHostName = useCallback(
(e, newName) => {
setHostName(newName);
@@ -547,168 +557,180 @@ export const AzureProvisionDialog: React.FC = () => {
[]
);
- const isDisAble = useMemo(() => {
+ const isNextDisabled = useMemo(() => {
return (
!currentSubscription ||
+ !currentResourceGroupName ||
!currentHostName ||
- errorHostName !== '' ||
- errorResourceGroupName !== '' ||
- !currentLocation
+ !currentLocation ||
+ errorResourceGroupName ||
+ errorHostName !== ''
);
- }, [currentSubscription, currentHostName, errorHostName, currentLocation, errorResourceGroupName]);
+ }, [
+ currentSubscription,
+ currentResourceGroupName,
+ currentHostName,
+ currentLocation,
+ errorResourceGroupName,
+ errorHostName,
+ ]);
const isSelectAddResources = useMemo(() => {
return enabledResources.length > 0 || requireResources.length > 0;
}, [enabledResources]);
+ const resourceGroupNames = resourceGroups?.map((r) => r.name) || [];
+
const PageFormConfig = (
-
-
- {
- setChoice(option);
- }}
- />
-
-
-
}>
- {subscriptionOption?.length > 0 && choice.key === 'create' && (
-