diff --git a/hub/src/locale/en.json b/hub/src/locale/en.json index d45baf7e7..82ba1d73c 100644 --- a/hub/src/locale/en.json +++ b/hub/src/locale/en.json @@ -81,6 +81,8 @@ "backend_type_description": "Select a backend type", "members_empty_message_title": "No members", "members_empty_message_text": "Select project's members", + "delete_project_confirm_title": "Delete project", + "delete_project_confirm_message": "Are you sure you want to delete this project?", "cli": "CLI", "aws": { "authorization": "Authorization", @@ -99,7 +101,10 @@ "s3_bucket_name_description": "Select an S3 bucket to store artifacts", "ec2_subnet_id": "Subnet", "ec2_subnet_id_description": "Select a subnet to run workflows in", - "ec2_subnet_id_placeholder": "Not selected" + "ec2_subnet_id_placeholder": "Not selected", + "extra_regions": "Additional regions", + "extra_regions_description": "Select additional regions to run workflows", + "extra_regions_placeholder": "Select regions" }, "azure" : { "authorization": "Authorization", @@ -291,7 +296,7 @@ "refresh_token_success_notification": "Token rotating is successful", "refresh_token_error_notification": "Token rotating error", "refresh_token_confirm_title": "Rotate token", - "refresh_token_confirm_message": "Do you sure want to rotate token?", + "refresh_token_confirm_message": "Are you sure you want to rotate token?", "refresh_token_button_label": "Rotate", "validation": { "user_name_format": "Only letters, numbers, - or _" @@ -315,7 +320,7 @@ }, "confirm_dialog": { "title": "Confirm delete", - "message": "Do you sure want to delete?" + "message": "Are you sure you want to delete?" } } diff --git a/hub/src/pages/Project/Details/Settings/index.tsx b/hub/src/pages/Project/Details/Settings/index.tsx index e7b8ae10c..bbf759e56 100644 --- a/hub/src/pages/Project/Details/Settings/index.tsx +++ b/hub/src/pages/Project/Details/Settings/index.tsx @@ -81,6 +81,8 @@ export const ProjectSettings: React.FC = () => { const renderAwsBackendDetails = (): React.ReactNode => { if (!data) return null; + const extraRegions = data.backend.extra_regions?.join(', '); + return (
@@ -100,7 +102,15 @@ export const ProjectSettings: React.FC = () => {
{t('projects.edit.aws.ec2_subnet_id')} -
{data.backend.ec2_subnet_id}
+
{data.backend.ec2_subnet_id || '-'}
+
+ +
+ {t('projects.edit.aws.extra_regions')} + +
+ {extraRegions || '-'} +
); diff --git a/hub/src/pages/Project/Details/index.tsx b/hub/src/pages/Project/Details/index.tsx index 74a45714d..471836aee 100644 --- a/hub/src/pages/Project/Details/index.tsx +++ b/hub/src/pages/Project/Details/index.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'; +import Box from '@cloudscape-design/components/box'; import { Button, ButtonProps, ConfirmationDialog, ContentLayout, DetailsHeader } from 'components'; @@ -82,7 +83,13 @@ export const ProjectDetails: React.FC = () => { - + {t('projects.edit.delete_project_confirm_message')}} + /> ); }; diff --git a/hub/src/pages/Project/EditBackend/index.tsx b/hub/src/pages/Project/EditBackend/index.tsx index ae9b2fd11..5741895a6 100644 --- a/hub/src/pages/Project/EditBackend/index.tsx +++ b/hub/src/pages/Project/EditBackend/index.tsx @@ -36,7 +36,7 @@ export const ProjectEditBackend: React.FC = () => { ]); const onCancelHandler = () => { - navigate(ROUTES.PROJECT.DETAILS.REPOSITORIES.FORMAT(paramProjectName)); + navigate(ROUTES.PROJECT.DETAILS.SETTINGS.FORMAT(paramProjectName)); }; const onSubmitHandler = async (data: Partial): Promise => { diff --git a/hub/src/pages/Project/Form/AWS/constants.tsx b/hub/src/pages/Project/Form/AWS/constants.tsx index 9ab2a1ed0..e6f35a925 100644 --- a/hub/src/pages/Project/Form/AWS/constants.tsx +++ b/hub/src/pages/Project/Form/AWS/constants.tsx @@ -9,6 +9,7 @@ export const FIELD_NAMES = { REGION_NAME: 'region_name', S3_BUCKET_NAME: 's3_bucket_name', EC2_SUBNET_ID: 'ec2_subnet_id', + EXTRA_REGIONS: 'extra_regions', }; export const CREDENTIALS_HELP = { @@ -79,3 +80,15 @@ export const SUBNET_HELP = { ), }; + +export const ADDITIONAL_REGIONS_HELP = { + header:

Additional regions

, + body: ( + <> +

+ dstack will try to provision the instance in additional regions if the primary region has no capacity.{' '} + Specifying additional regions increases the chances of provisioning spot instances. +

+ + ), +}; diff --git a/hub/src/pages/Project/Form/AWS/index.tsx b/hub/src/pages/Project/Form/AWS/index.tsx index 14f0d1aba..32186c89f 100644 --- a/hub/src/pages/Project/Form/AWS/index.tsx +++ b/hub/src/pages/Project/Form/AWS/index.tsx @@ -3,7 +3,17 @@ import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { debounce } from 'lodash'; -import { FormInput, FormS3BucketSelector, FormSelect, FormSelectOptions, InfoLink, SpaceBetween, Spinner } from 'components'; +import { + FormInput, + FormMultiselect, + FormMultiselectOptions, + FormS3BucketSelector, + FormSelect, + FormSelectOptions, + InfoLink, + SpaceBetween, + Spinner, +} from 'components'; import { useHelpPanel, useNotifications } from 'hooks'; import { isRequestFormErrors2, isRequestFormFieldError } from 'libs'; @@ -11,7 +21,7 @@ import { useBackendValuesMutation } from 'services/project'; import { AWSCredentialTypeEnum } from 'types'; import useIsMounted from '../../../../hooks/useIsMounted'; -import { BUCKET_HELP, CREDENTIALS_HELP, FIELD_NAMES, REGION_HELP, SUBNET_HELP } from './constants'; +import { ADDITIONAL_REGIONS_HELP, BUCKET_HELP, CREDENTIALS_HELP, FIELD_NAMES, REGION_HELP, SUBNET_HELP } from './constants'; import { IProps } from './types'; @@ -25,6 +35,7 @@ export const AWSBackend: React.FC = ({ loading }) => { const [regions, setRegions] = useState([]); const [buckets, setBuckets] = useState([]); const [subnets, setSubnets] = useState([]); + const [extraRegions, setExtraRegions] = useState([]); const [availableDefaultCredentials, setAvailableDefaultCredentials] = useState(null); const lastUpdatedField = useRef(null); const isFirstRender = useRef(true); @@ -105,6 +116,14 @@ export const AWSBackend: React.FC = ({ loading }) => { if (response.ec2_subnet_id?.selected !== undefined) { setValue(`backend.${FIELD_NAMES.EC2_SUBNET_ID}`, response.ec2_subnet_id.selected ?? ''); } + + if (response.extra_regions?.values) { + setExtraRegions(response.extra_regions.values); + } + + if (response.extra_regions?.selected !== undefined) { + setValue(`backend.${FIELD_NAMES.EXTRA_REGIONS}`, response.extra_regions.selected); + } } catch (errorResponse) { console.log('fetch backends values error:', errorResponse); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -262,6 +281,19 @@ export const AWSBackend: React.FC = ({ loading }) => { options={subnets} secondaryControl={renderSpinner()} /> + + openHelpPanel(ADDITIONAL_REGIONS_HELP)} />} + label={t('projects.edit.aws.extra_regions')} + description={t('projects.edit.aws.extra_regions_description')} + placeholder={t('projects.edit.aws.extra_regions_placeholder')} + control={control} + name={`backend.${FIELD_NAMES.EXTRA_REGIONS}`} + onChange={getOnChangeSelectField(FIELD_NAMES.EXTRA_REGIONS)} + disabled={getDisabledByFieldName(FIELD_NAMES.EXTRA_REGIONS)} + secondaryControl={renderSpinner()} + options={extraRegions} + /> ); }; diff --git a/hub/src/types/project.d.ts b/hub/src/types/project.d.ts index 349d305c4..73a9ad0d4 100644 --- a/hub/src/types/project.d.ts +++ b/hub/src/types/project.d.ts @@ -27,6 +27,10 @@ declare interface IProjectAwsBackendValues { selected?: string | null, values: { value: string, label: string}[] } | null, + extra_regions: { + selected?: string[] | null, + values: { value: string, label: string}[] + } | null, } declare interface IProjectAzureBackendValues { @@ -107,6 +111,7 @@ declare interface TProjectBackendAWS { region_name: string, s3_bucket_name: string, ec2_subnet_id: string | null, + extra_regions: string[], } enum AzureCredentialTypeEnum {