mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 22:05:23 +02:00
refactor(azure/aci): migrate create view to react [EE-2188] (#6371)
This commit is contained in:
parent
1bb02eea59
commit
6f6f78fbe5
53 changed files with 1476 additions and 571 deletions
|
@ -0,0 +1,43 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
|
||||
import { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
|
||||
|
||||
jest.mock('@uirouter/react', () => ({
|
||||
...jest.requireActual('@uirouter/react'),
|
||||
useCurrentStateAndParams: jest.fn(() => ({
|
||||
params: { endpointId: 5 },
|
||||
})),
|
||||
}));
|
||||
|
||||
test('submit button should be disabled when name or image is missing', async () => {
|
||||
const user = new UserViewModel({ Username: 'user' });
|
||||
|
||||
const { findByText, getByText, getByLabelText } = renderWithQueryClient(
|
||||
<UserContext.Provider value={{ user }}>
|
||||
<CreateContainerInstanceForm />
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
await expect(findByText(/Azure settings/)).resolves.toBeVisible();
|
||||
|
||||
const button = getByText(/Deploy the container/);
|
||||
expect(button).toBeVisible();
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
const nameInput = getByLabelText(/name/i);
|
||||
userEvent.type(nameInput, 'name');
|
||||
|
||||
const imageInput = getByLabelText(/image/i);
|
||||
userEvent.type(imageInput, 'image');
|
||||
|
||||
await expect(findByText(/Deploy the container/)).resolves.toBeEnabled();
|
||||
|
||||
expect(nameInput).toHaveValue('name');
|
||||
userEvent.clear(nameInput);
|
||||
|
||||
await expect(findByText(/Deploy the container/)).resolves.toBeDisabled();
|
||||
});
|
|
@ -0,0 +1,219 @@
|
|||
import { Field, Form, Formik } from 'formik';
|
||||
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Input, Select } from '@/portainer/components/form-components/Input';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { InputListError } from '@/portainer/components/form-components/InputList/InputList';
|
||||
import { AccessControlForm } from '@/portainer/components/accessControlForm';
|
||||
import { ContainerInstanceFormValues } from '@/azure/types';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { isAdmin, useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { validationSchema } from './CreateContainerInstanceForm.validation';
|
||||
import { PortMapping, PortsMappingField } from './PortsMappingField';
|
||||
import { useLoadFormState } from './useLoadFormState';
|
||||
import {
|
||||
getSubscriptionLocations,
|
||||
getSubscriptionResourceGroups,
|
||||
} from './utils';
|
||||
import { useCreateInstance } from './useCreateInstanceMutation';
|
||||
|
||||
export function CreateContainerInstanceForm() {
|
||||
const {
|
||||
params: { endpointId: environmentId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
if (!environmentId) {
|
||||
throw new Error('endpointId url param is required');
|
||||
}
|
||||
|
||||
const { user } = useUser();
|
||||
const isUserAdmin = isAdmin(user);
|
||||
|
||||
const { initialValues, isLoading, providers, subscriptions, resourceGroups } =
|
||||
useLoadFormState(environmentId, isUserAdmin);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync } = useCreateInstance(
|
||||
resourceGroups,
|
||||
environmentId,
|
||||
user?.Id
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik<ContainerInstanceFormValues>
|
||||
initialValues={initialValues}
|
||||
validationSchema={() => validationSchema(isUserAdmin)}
|
||||
onSubmit={onSubmit}
|
||||
validateOnMount
|
||||
validateOnChange
|
||||
enableReinitialize
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => (
|
||||
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
|
||||
<FormSectionTitle>Azure settings</FormSectionTitle>
|
||||
<FormControl
|
||||
label="Subscription"
|
||||
inputId="subscription-input"
|
||||
errors={errors.subscription}
|
||||
>
|
||||
<Field
|
||||
name="subscription"
|
||||
as={Select}
|
||||
id="subscription-input"
|
||||
options={subscriptions}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Resource group"
|
||||
inputId="resourceGroup-input"
|
||||
errors={errors.resourceGroup}
|
||||
>
|
||||
<Field
|
||||
name="resourceGroup"
|
||||
as={Select}
|
||||
id="resourceGroup-input"
|
||||
options={getSubscriptionResourceGroups(
|
||||
values.subscription,
|
||||
resourceGroups
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Location"
|
||||
inputId="location-input"
|
||||
errors={errors.location}
|
||||
>
|
||||
<Field
|
||||
name="location"
|
||||
as={Select}
|
||||
id="location-input"
|
||||
options={getSubscriptionLocations(values.subscription, providers)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormSectionTitle>Container configuration</FormSectionTitle>
|
||||
|
||||
<FormControl label="Name" inputId="name-input" errors={errors.name}>
|
||||
<Field
|
||||
name="name"
|
||||
as={Input}
|
||||
id="name-input"
|
||||
placeholder="e.g. myContainer"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Image"
|
||||
inputId="image-input"
|
||||
errors={errors.image}
|
||||
>
|
||||
<Field
|
||||
name="image"
|
||||
as={Input}
|
||||
id="image-input"
|
||||
placeholder="e.g. nginx:alpine"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="OS" inputId="os-input" errors={errors.os}>
|
||||
<Field
|
||||
name="os"
|
||||
as={Select}
|
||||
id="os-input"
|
||||
options={[
|
||||
{ label: 'Linux', value: 'Linux' },
|
||||
{ label: 'Windows', value: 'Windows' },
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<PortsMappingField
|
||||
value={values.ports}
|
||||
onChange={(value) => setFieldValue('ports', value)}
|
||||
errors={errors.ports as InputListError<PortMapping>[]}
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12 small text-muted">
|
||||
This will automatically deploy a container with a public IP
|
||||
address
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormSectionTitle>Container Resources</FormSectionTitle>
|
||||
|
||||
<FormControl label="CPU" inputId="cpu-input" errors={errors.cpu}>
|
||||
<Field
|
||||
name="cpu"
|
||||
as={Input}
|
||||
id="cpu-input"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Memory"
|
||||
inputId="cpu-input"
|
||||
errors={errors.memory}
|
||||
>
|
||||
<Field
|
||||
name="memory"
|
||||
as={Input}
|
||||
id="memory-input"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<AccessControlForm
|
||||
formNamespace="accessControl"
|
||||
onChange={(values) => setFieldValue('accessControl', values)}
|
||||
values={values.accessControl}
|
||||
errors={errors.accessControl}
|
||||
/>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Deployment in progress..."
|
||||
>
|
||||
<i className="fa fa-plus space-right" aria-hidden="true" />
|
||||
Deploy the container
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
|
||||
async function onSubmit(values: ContainerInstanceFormValues) {
|
||||
try {
|
||||
await mutateAsync(values);
|
||||
notifications.success('Container successfully created', values.name);
|
||||
router.stateService.go('azure.containerinstances');
|
||||
} catch (e) {
|
||||
notifications.error('Failure', e as Error, 'Unable to create container');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { object, string, number, boolean } from 'yup';
|
||||
|
||||
import { validationSchema as accessControlSchema } from '@/portainer/components/accessControlForm/AccessControlForm.validation';
|
||||
|
||||
import { validationSchema as portsSchema } from './PortsMappingField.validation';
|
||||
|
||||
export function validationSchema(isAdmin: boolean) {
|
||||
return object().shape({
|
||||
name: string().required('Name is required.'),
|
||||
image: string().required('Image is required.'),
|
||||
subscription: string().required('Subscription is required.'),
|
||||
resourceGroup: string().required('Resource group is required.'),
|
||||
location: string().required('Location is required.'),
|
||||
os: string().oneOf(['Linux', 'Windows']),
|
||||
cpu: number().positive(),
|
||||
memory: number().positive(),
|
||||
allocatePublicIP: boolean(),
|
||||
ports: portsSchema(),
|
||||
accessControl: accessControlSchema(isAdmin),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item .inputs {
|
||||
}
|
||||
|
||||
.item .errors {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { ButtonSelector } from '@/portainer/components/form-components/ButtonSelector/ButtonSelector';
|
||||
import { FormError } from '@/portainer/components/form-components/FormError';
|
||||
import { InputGroup } from '@/portainer/components/form-components/InputGroup';
|
||||
import { InputList } from '@/portainer/components/form-components/InputList';
|
||||
import {
|
||||
InputListError,
|
||||
ItemProps,
|
||||
} from '@/portainer/components/form-components/InputList/InputList';
|
||||
|
||||
import styles from './PortsMappingField.module.css';
|
||||
|
||||
type Protocol = 'TCP' | 'UDP';
|
||||
|
||||
export interface PortMapping {
|
||||
host: string;
|
||||
container: string;
|
||||
protocol: Protocol;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: PortMapping[];
|
||||
onChange(value: PortMapping[]): void;
|
||||
errors?: InputListError<PortMapping>[] | string;
|
||||
}
|
||||
|
||||
export function PortsMappingField({ value, onChange, errors }: Props) {
|
||||
return (
|
||||
<>
|
||||
<InputList<PortMapping>
|
||||
label="Port mapping"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
addLabel="map additional port"
|
||||
itemBuilder={() => ({ host: '', container: '', protocol: 'TCP' })}
|
||||
item={Item}
|
||||
errors={errors}
|
||||
/>
|
||||
{typeof errors === 'string' && (
|
||||
<div className="form-group col-md-12">
|
||||
<FormError>{errors}</FormError>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({ onChange, item, error }: ItemProps<PortMapping>) {
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.inputs}>
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>host</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
placeholder="e.g. 80"
|
||||
value={item.host}
|
||||
onChange={(e) => handleChange('host', e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<span style={{ margin: '0 10px 0 10px' }}>
|
||||
<i className="fa fa-long-arrow-alt-right" aria-hidden="true" />
|
||||
</span>
|
||||
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>container</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
placeholder="e.g. 80"
|
||||
value={item.container}
|
||||
onChange={(e) => handleChange('container', e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<ButtonSelector<Protocol>
|
||||
onChange={(value) => handleChange('protocol', value)}
|
||||
value={item.protocol}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
</div>
|
||||
{!!error && (
|
||||
<div className={styles.errors}>
|
||||
<FormError>{Object.values(error)[0]}</FormError>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleChange(name: string, value: string) {
|
||||
onChange({ ...item, [name]: value });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { array, object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return array(
|
||||
object().shape({
|
||||
host: string().required('host is required'),
|
||||
container: string().required('container is required'),
|
||||
protocol: string().oneOf(['TCP', 'UDP']),
|
||||
})
|
||||
).min(1, 'At least one port binding is required');
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
|
|
@ -0,0 +1,61 @@
|
|||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { createContainerGroup } from '@/azure/services/container-groups.service';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import {
|
||||
ContainerGroup,
|
||||
ContainerInstanceFormValues,
|
||||
ResourceGroup,
|
||||
} from '@/azure/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { applyResourceControl } from '@/portainer/resource-control/resource-control.service';
|
||||
|
||||
import { getSubscriptionResourceGroups } from './utils';
|
||||
|
||||
export function useCreateInstance(
|
||||
resourceGroups: {
|
||||
[k: string]: ResourceGroup[];
|
||||
},
|
||||
environmentId: EnvironmentId,
|
||||
userId?: UserId
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<ContainerGroup, unknown, ContainerInstanceFormValues>(
|
||||
(values) => {
|
||||
if (!values.subscription) {
|
||||
throw new PortainerError('subscription is required');
|
||||
}
|
||||
|
||||
const subscriptionResourceGroup = getSubscriptionResourceGroups(
|
||||
values.subscription,
|
||||
resourceGroups
|
||||
);
|
||||
const resourceGroup = subscriptionResourceGroup.find(
|
||||
(r) => r.value === values.resourceGroup
|
||||
);
|
||||
if (!resourceGroup) {
|
||||
throw new PortainerError('resource group not found');
|
||||
}
|
||||
|
||||
return createContainerGroup(
|
||||
values,
|
||||
environmentId,
|
||||
values.subscription,
|
||||
resourceGroup.label
|
||||
);
|
||||
},
|
||||
{
|
||||
async onSuccess(containerGroup, values) {
|
||||
if (!userId) {
|
||||
throw new Error('missing user id');
|
||||
}
|
||||
|
||||
const resourceControl = containerGroup.Portainer.ResourceControl;
|
||||
const accessControlData = values.accessControl;
|
||||
await applyResourceControl(userId, accessControlData, resourceControl);
|
||||
queryClient.invalidateQueries(['azure', 'container-instances']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import { useQueries, useQuery } from 'react-query';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import { Option } from '@/portainer/components/form-components/Input/Select';
|
||||
import { getResourceGroups } from '@/azure/services/resource-groups.service';
|
||||
import { getSubscriptions } from '@/azure/services/subscription.service';
|
||||
import { getContainerInstanceProvider } from '@/azure/services/provider.service';
|
||||
import { ContainerInstanceFormValues, Subscription } from '@/azure/types';
|
||||
import { parseFromResourceControl } from '@/portainer/components/accessControlForm/model';
|
||||
|
||||
import {
|
||||
getSubscriptionLocations,
|
||||
getSubscriptionResourceGroups,
|
||||
} from './utils';
|
||||
|
||||
export function useLoadFormState(
|
||||
environmentId: EnvironmentId,
|
||||
isUserAdmin: boolean
|
||||
) {
|
||||
const { subscriptions, isLoading: isLoadingSubscriptions } =
|
||||
useSubscriptions(environmentId);
|
||||
const { resourceGroups, isLoading: isLoadingResourceGroups } =
|
||||
useResourceGroups(environmentId, subscriptions);
|
||||
const { providers, isLoading: isLoadingProviders } = useProviders(
|
||||
environmentId,
|
||||
subscriptions
|
||||
);
|
||||
|
||||
const subscriptionOptions =
|
||||
subscriptions?.map((s) => ({
|
||||
value: s.subscriptionId,
|
||||
label: s.displayName,
|
||||
})) || [];
|
||||
|
||||
const initSubscriptionId = getFirstValue(subscriptionOptions);
|
||||
|
||||
const subscriptionResourceGroups = getSubscriptionResourceGroups(
|
||||
initSubscriptionId,
|
||||
resourceGroups
|
||||
);
|
||||
|
||||
const subscriptionLocations = getSubscriptionLocations(
|
||||
initSubscriptionId,
|
||||
providers
|
||||
);
|
||||
|
||||
const initialValues: ContainerInstanceFormValues = {
|
||||
name: '',
|
||||
location: getFirstValue(subscriptionLocations),
|
||||
subscription: initSubscriptionId,
|
||||
resourceGroup: getFirstValue(subscriptionResourceGroups),
|
||||
image: '',
|
||||
os: 'Linux',
|
||||
memory: 1,
|
||||
cpu: 1,
|
||||
ports: [{ container: '80', host: '80', protocol: 'TCP' }],
|
||||
allocatePublicIP: true,
|
||||
accessControl: parseFromResourceControl(isUserAdmin),
|
||||
};
|
||||
|
||||
return {
|
||||
isUserAdmin,
|
||||
initialValues,
|
||||
subscriptions: subscriptionOptions,
|
||||
resourceGroups,
|
||||
providers,
|
||||
isLoading:
|
||||
isLoadingProviders || isLoadingResourceGroups || isLoadingSubscriptions,
|
||||
};
|
||||
|
||||
function getFirstValue<T extends string | number>(arr: Option<T>[]) {
|
||||
if (arr.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return arr[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
function useSubscriptions(environmentId: EnvironmentId) {
|
||||
const { data, isError, error, isLoading } = useQuery(
|
||||
'azure.subscriptions',
|
||||
() => getSubscriptions(environmentId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return { subscriptions: data || [], isLoading };
|
||||
}
|
||||
|
||||
function useResourceGroups(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptions: Subscription[]
|
||||
) {
|
||||
const queries = useQueries(
|
||||
subscriptions.map((subscription) => ({
|
||||
queryKey: ['azure.resourceGroups', subscription.subscriptionId],
|
||||
queryFn: () =>
|
||||
getResourceGroups(environmentId, subscription.subscriptionId),
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const failedQuery = queries.find((q) => q.error);
|
||||
if (failedQuery) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
failedQuery.error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [queries]);
|
||||
|
||||
return {
|
||||
resourceGroups: Object.fromEntries(
|
||||
queries.map((q, index) => [
|
||||
subscriptions[index].subscriptionId,
|
||||
q.data || [],
|
||||
])
|
||||
),
|
||||
isLoading: queries.some((q) => q.isLoading),
|
||||
};
|
||||
}
|
||||
|
||||
function useProviders(
|
||||
environmentId: EnvironmentId,
|
||||
subscriptions: Subscription[]
|
||||
) {
|
||||
const queries = useQueries(
|
||||
subscriptions.map((subscription) => ({
|
||||
queryKey: [
|
||||
'azure.containerInstanceProvider',
|
||||
subscription.subscriptionId,
|
||||
],
|
||||
queryFn: () =>
|
||||
getContainerInstanceProvider(
|
||||
environmentId,
|
||||
subscription.subscriptionId
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const failedQuery = queries.find((q) => q.error);
|
||||
if (failedQuery) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
failedQuery.error as PortainerError,
|
||||
'Unable to retrieve Azure resources'
|
||||
);
|
||||
}
|
||||
}, [queries]);
|
||||
|
||||
return {
|
||||
providers: Object.fromEntries(
|
||||
queries.map((q, index) => [subscriptions[index].subscriptionId, q.data])
|
||||
),
|
||||
isLoading: queries.some((q) => q.isLoading),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { ProviderViewModel } from '@/azure/models/provider';
|
||||
import { ResourceGroup } from '@/azure/types';
|
||||
|
||||
export function getSubscriptionResourceGroups(
|
||||
subscriptionId?: string,
|
||||
resourceGroups?: Record<string, ResourceGroup[]>
|
||||
) {
|
||||
if (!subscriptionId || !resourceGroups || !resourceGroups[subscriptionId]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return resourceGroups[subscriptionId].map(({ name, id }) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getSubscriptionLocations(
|
||||
subscriptionId?: string,
|
||||
containerInstanceProviders?: Record<string, ProviderViewModel | undefined>
|
||||
) {
|
||||
if (!subscriptionId || !containerInstanceProviders) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const provider = containerInstanceProviders[subscriptionId];
|
||||
if (!provider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return provider.locations.map((location) => ({
|
||||
value: location,
|
||||
label: location,
|
||||
}));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue