Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
227 changes: 184 additions & 43 deletions Composer/packages/client/src/components/ManageService/ManageService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/Choi
import { ProvisionHandoff } from '@bfc/ui-shared';
import sortBy from 'lodash/sortBy';
import { NeutralColors } from '@uifabric/fluent-theme';
import { AzureTenant } from '@botframework-composer/types';

import TelemetryClient from '../../telemetry/TelemetryClient';
import { AuthClient } from '../../utils/authClient';
Expand Down Expand Up @@ -59,7 +60,12 @@ type KeyRec = {
resourceGroup: string;
key: string;
};

enum Page {
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
Intro = 1,
Subscription = 2,
ResourceCreation = 2.1,
Outcome = 3,
}
const dropdownStyles = { dropdown: { width: '100%', marginBottom: 10 } };
const inputStyles = { root: { width: '100%', marginBottom: 10 } };
const summaryLabelStyles = { display: 'block', color: '#605E5C', fontSize: 14 };
Expand All @@ -74,13 +80,15 @@ export const ManageService = (props: ManageServiceProps) => {

const { setApplicationLevelError } = useRecoilValue(dispatcherState);
const [subscriptionId, setSubscription] = useState<string>('');
const [tenantId, setTenantId] = useState<string | undefined>(undefined);
const [resourceGroups, setResourceGroups] = useState<any[]>([]);
const [createResourceGroup, setCreateResourceGroup] = useState<boolean>(false);
const [newResourceGroupName, setNewResourceGroupName] = useState<string>('');
const [resourceGroupKey, setResourceGroupKey] = useState<string>('');
const [resourceGroup, setResourceGroup] = useState<string>('');
const [tier, setTier] = useState<string>('');

const [allTenants, setAllTenants] = useState<AzureTenant[]>([]);
const [tenantsErrorMessage, setTenantsErrorMessage] = useState<string | undefined>(undefined);
const [showHandoff, setShowHandoff] = useState<boolean>(false);
const [resourceName, setResourceName] = useState<string>('');
const [loading, setLoading] = useState<string | undefined>(undefined);
Expand All @@ -89,6 +97,7 @@ export const ManageService = (props: ManageServiceProps) => {
const [key, setKey] = useState<string>('');
const [region, setRegion] = useState<string>('');
const [availableSubscriptions, setAvailableSubscriptions] = useState<Subscription[]>([]);
const [subscriptionsErrorMessage, setSubscriptionsErrorMessage] = useState<string>();
const [keys, setKeys] = useState<KeyRec[]>([]);

const [currentPage, setCurrentPage] = useState<number>(1);
Expand Down Expand Up @@ -132,6 +141,69 @@ export const ManageService = (props: ManageServiceProps) => {
return [];
}
};
useEffect(() => {
if (!userShouldProvideTokens()) {
AuthClient.getTenants()
.then((tenants) => {
setAllTenants(tenants);
if (tenants.length === 0) {
setTenantsErrorMessage(formatMessage('No Azure Directories were found.'));
} else if (tenants.length === 1) {
setTenantId(tenants[0].tenantId);
} else {
setTenantsErrorMessage(undefined);
}
})
.catch((err) => {
setTenantsErrorMessage(
formatMessage('There was a problem loading Azure directories. {errMessage}', {
errMessage: err.message || err.toString(),
})
);
});
}
}, []);
useEffect(() => {
Comment thread
VamsiModem marked this conversation as resolved.
if (tenantId) {
AuthClient.getARMTokenForTenant(tenantId)
.then((token) => {
setToken(token);
setTenantsErrorMessage(undefined);
})
.catch((err) => {
setTenantId(undefined);
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
setTenantsErrorMessage(
formatMessage(
'There was a problem getting the access token for the current Azure directory. {errMessage}',
{
errMessage: err.message || err.toString(),
}
)
);
setTenantsErrorMessage(err.message || err.toString());
});
}
}, [tenantId]);
useEffect(() => {
Comment thread
VamsiModem marked this conversation as resolved.
if (token) {
setAvailableSubscriptions([]);
setSubscriptionsErrorMessage(undefined);
getSubscriptions(token)
.then((data) => {
setAvailableSubscriptions(data);
if (data.length === 0) {
setSubscriptionsErrorMessage(
formatMessage(
'Your subscription list is empty, please add your subscription, or login with another account.'
)
);
}
})
.catch((err) => {
setSubscriptionsErrorMessage(err.message);
});
}
}, [token]);

const hasAuth = async () => {
let newtoken = '';
Expand All @@ -140,22 +212,11 @@ export const ManageService = (props: ManageServiceProps) => {
setShowAuthDialog(true);
}
newtoken = getTokenFromCache('accessToken');
} else {
newtoken = await AuthClient.getAccessToken(armScopes);
}

setToken(newtoken);

if (newtoken) {
// reset the list
setAvailableSubscriptions([]);

// fetch list of available subscriptions
setAvailableSubscriptions(await getSubscriptions(newtoken));

// go on to the next page post-auth
setCurrentPage(2);
setToken(newtoken);
}
setCurrentPage(2);
};

useEffect(() => {
Expand Down Expand Up @@ -441,13 +502,13 @@ export const ManageService = (props: ManageServiceProps) => {
);
};

const renderPageOne = () => {
const pageOne = () => {
return (
<div>
<div css={dialogBodyStyles}>
<p css={{ marginTop: 0 }}>
{formatMessage(
'Choose from existing {service} keys, create a new {service} resource, or generate a request to handoff to your Azure admin. ',
'Select your Azure directory, then choose the subscription where your existing resource is located and the keys you want to use.',
{ service: props.serviceName }
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
)}
{props.learnMore ? (
Expand All @@ -472,13 +533,13 @@ export const ManageService = (props: ManageServiceProps) => {
);
};

const renderPageChoose = () => {
const chooseResource = () => {
return (
<div>
<div css={dialogBodyStyles}>
<p css={{ marginTop: 0 }}>
{formatMessage(
'Choose from existing {service} keys, create a new {service} resource, or generate a request to handoff to your Azure admin. ',
'Select your Azure directory, then choose the subscription where you’d like your new {service} resource.',
{ service: props.serviceName }
)}
{props.learnMore ? (
Expand All @@ -489,8 +550,22 @@ export const ManageService = (props: ManageServiceProps) => {
</p>
<div css={mainElementStyle}>
<Dropdown
required
disabled={allTenants.length === 1 || tenantId != null}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allTenants.length === 1 || !tenantId
(dont do != over !==)
Here you also need to check if tenantId doesn't have values: null or undefined or empty string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Geoff Cox (Microsoft) (@GeoffCoxMSFT) hidden vs. disabled dropdown, which one should it be?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the azureProvisionDialog, we are just disabling the dropdown, when there is only one tenant or if it's invalid tenantId.

errorMessage={tenantsErrorMessage}
label={formatMessage('Azure directory')}
options={allTenants.map((t) => ({ key: t.tenantId, text: t.displayName }))}
selectedKey={tenantId}
styles={dropdownStyles}
onChange={(_e, o) => {
setTenantId(o?.key as string);
}}
/>
<Dropdown
required
disabled={!(availableSubscriptions?.length > 0)}
label={formatMessage('Select subscription')}
errorMessage={subscriptionsErrorMessage}
label={formatMessage('Subscription')}
options={
availableSubscriptions
?.filter((p) => p.subscriptionId && p.displayName)
Expand Down Expand Up @@ -551,7 +626,7 @@ export const ManageService = (props: ManageServiceProps) => {
);
};

const renderPageCreate = () => {
const resourceCreator = () => {
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
return (
<div>
<div css={dialogBodyStyles}>
Expand All @@ -563,21 +638,6 @@ export const ManageService = (props: ManageServiceProps) => {
</p>

<div css={mainElementStyle}>
<Dropdown
disabled={loading !== undefined || !(availableSubscriptions?.length > 0)}
label={formatMessage('Select subscription')}
options={
availableSubscriptions
?.filter((p) => p.subscriptionId && p.displayName)
.map((p) => {
return { key: p.subscriptionId ?? '', text: p.displayName ?? 'Unnamed' };
}) ?? []
}
placeholder={formatMessage('Select one')}
selectedKey={subscriptionId}
styles={dropdownStyles}
onChange={onChangeSubscription}
/>
<Dropdown
disabled={!subscriptionId || resourceGroups.length === 0 || loading !== undefined}
label={formatMessage('Resource group')}
Expand Down Expand Up @@ -655,7 +715,7 @@ export const ManageService = (props: ManageServiceProps) => {
<DefaultButton
disabled={loading !== undefined}
text={formatMessage('Back')}
onClick={() => setCurrentPage(1)}
onClick={() => setCurrentPage(2)}
/>
<PrimaryButton
disabled={
Expand All @@ -675,7 +735,7 @@ export const ManageService = (props: ManageServiceProps) => {
);
};

const renderPageThree = () => {
const pageThree = () => {
return (
<div>
<div css={dialogBodyStyles}>
Expand All @@ -695,6 +755,90 @@ export const ManageService = (props: ManageServiceProps) => {
);
};

const subscriptionSelector = () => {
return (
<div>
<div css={dialogBodyStyles}>
<p css={{ marginTop: 0 }}>
{formatMessage(
'Select your Azure directory, then choose the subscription where you’d like your new {service} resource.',
{ service: props.serviceName }
)}
{props.learnMore ? (
<Link href={props.learnMore} target={'_blank'}>
{formatMessage('Learn more')}
</Link>
) : null}
</p>
<div css={mainElementStyle}>
<Dropdown
required
disabled={allTenants.length === 1 || tenantId != null}
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
errorMessage={tenantsErrorMessage}
label={formatMessage('Azure directory')}
options={allTenants.map((t) => ({ key: t.tenantId, text: t.displayName }))}
selectedKey={tenantId}
styles={dropdownStyles}
onChange={(_e, o) => {
setTenantId(o?.key as string);
}}
/>
<Dropdown
required
disabled={!(availableSubscriptions?.length > 0)}
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
errorMessage={subscriptionsErrorMessage}
label={formatMessage('Subscription')}
options={
availableSubscriptions
?.filter((p) => p.subscriptionId && p.displayName)
.map((p) => {
return { key: p.subscriptionId ?? '', text: p.displayName ?? 'Unnamed' };
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
}) ?? []
}
placeholder={formatMessage('Select one')}
selectedKey={subscriptionId}
styles={dropdownStyles}
onChange={onChangeSubscription}
/>
</div>
</div>
<DialogFooter>
{loading && <Spinner label={loading} labelPosition="right" styles={{ root: { float: 'left' } }} />}
<DefaultButton
disabled={loading !== undefined}
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
text={formatMessage('Back')}
onClick={() => setCurrentPage(1)}
/>
<PrimaryButton
disabled={loading !== undefined || !tenantId || !subscriptionId}
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
text={formatMessage('Next')}
onClick={() => setCurrentPage(2.1)}
/>
<DefaultButton disabled={loading !== undefined} text={formatMessage('Cancel')} onClick={props.onDismiss} />
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
</DialogFooter>
</div>
);
};

const getPage = () => {
switch (currentPage as Page) {
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
case Page.Intro:
return pageOne();
case Page.Subscription: {
if (nextAction === 'choose') {
return chooseResource();
}
return subscriptionSelector();
}
case Page.ResourceCreation:
return resourceCreator();
case Page.Outcome:
return pageThree();
default:
return null;
}
};

return (
<Fragment>
{showAuthDialog && (
Expand Down Expand Up @@ -726,7 +870,7 @@ export const ManageService = (props: ManageServiceProps) => {
dialogContentProps={{
type: DialogType.normal,
title:
currentPage === 2
nextAction === 'create'
? formatMessage('Create new {service} resource', { service: props.serviceName })
: formatMessage('Select {service} keys', { service: props.serviceName }),
}}
Expand All @@ -737,10 +881,7 @@ export const ManageService = (props: ManageServiceProps) => {
}}
onDismiss={loading ? () => {} : props.onDismiss}
>
{currentPage === 1 && renderPageOne()}
{currentPage === 2 && nextAction === 'choose' && renderPageChoose()}
{currentPage === 2 && nextAction === 'create' && renderPageCreate()}
{currentPage === 3 && renderPageThree()}
{getPage()}
Comment thread
VamsiModem marked this conversation as resolved.
Outdated
</Dialog>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ export const RootBotExternalService: React.FC<RootBotExternalServiceProps> = (pr
),
a2: ({ children }) => (
<Link
key="luis-endpoint-key-info"
key="luis-endpoint-key-limits-info"
href={'https://aka.ms/composer-settings-luislimits'}
target="_blank"
>
Expand Down