diff --git a/frontend/src/libs/password.ts b/frontend/src/libs/password.ts new file mode 100644 index 000000000..e430cdcf1 --- /dev/null +++ b/frontend/src/libs/password.ts @@ -0,0 +1,106 @@ +const UPPERCASE_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const LOWERCASE_LETTERS = 'abcdefghijklmnopqrstuvwxyz'; +const NUMBERS = '0123456789'; +const SPECIAL_CHARACTERS = '@#$^_+-'; + +interface PasswordOptions { + length: number; + includeUppercase?: boolean; + includeLowercase?: boolean; + includeNumbers?: boolean; + includeSpecial?: boolean; +} +function generatePassword(options: PasswordOptions): string { + const { length, includeUppercase = true, includeLowercase = true, includeNumbers = true, includeSpecial = true } = options; + + let allowedChars = ''; + + if (includeUppercase) allowedChars += UPPERCASE_LETTERS; + if (includeLowercase) allowedChars += LOWERCASE_LETTERS; + if (includeNumbers) allowedChars += NUMBERS; + if (includeSpecial) allowedChars += SPECIAL_CHARACTERS; + + if (allowedChars.length === 0) { + throw new Error('No character type is selected for the password'); + } + + if (length < 4) { + throw new Error('The password must be at least 4 characters long'); + } + + let password = ''; + const randomValues = new Uint32Array(length); + + crypto.getRandomValues(randomValues); + + for (let i = 0; i < length; i++) { + const randomIndex = randomValues[i] % allowedChars.length; + password += allowedChars[randomIndex]; + } + + return password; +} + +function generateSimplePassword(length: number): string { + const ALL_CHARS = UPPERCASE_LETTERS + LOWERCASE_LETTERS + NUMBERS + SPECIAL_CHARACTERS; + + if (length < 1) { + throw new Error('The password length must be a positive number'); + } + + let password = ''; + const randomValues = new Uint32Array(length); + + crypto.getRandomValues(randomValues); + + for (let i = 0; i < length; i++) { + const randomIndex = randomValues[i] % ALL_CHARS.length; + password += ALL_CHARS[randomIndex]; + } + + return password; +} + +function generateSecurePassword(length: number): string { + if (length < 4) { + throw new Error('The minimum length for a secure password is 4 characters'); + } + + const charSets = [UPPERCASE_LETTERS, LOWERCASE_LETTERS, NUMBERS, SPECIAL_CHARACTERS]; + + let password = ''; + password += UPPERCASE_LETTERS[Math.floor(Math.random() * UPPERCASE_LETTERS.length)]; + password += LOWERCASE_LETTERS[Math.floor(Math.random() * LOWERCASE_LETTERS.length)]; + password += NUMBERS[Math.floor(Math.random() * NUMBERS.length)]; + password += SPECIAL_CHARACTERS[Math.floor(Math.random() * SPECIAL_CHARACTERS.length)]; + + const ALL_CHARS = charSets.join(''); + const remainingLength = length - 4; + + if (remainingLength > 0) { + const randomValues = new Uint32Array(remainingLength); + crypto.getRandomValues(randomValues); + + for (let i = 0; i < remainingLength; i++) { + const randomIndex = randomValues[i] % ALL_CHARS.length; + password += ALL_CHARS[randomIndex]; + } + } + + return password + .split('') + .sort(() => Math.random() - 0.5) + .join(''); +} + +export { + generatePassword, + generateSimplePassword, + generateSecurePassword, + UPPERCASE_LETTERS, + LOWERCASE_LETTERS, + NUMBERS, + SPECIAL_CHARACTERS, +}; + +export type { PasswordOptions }; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 56c5e2efc..fd06c7dca 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -1,774 +1,780 @@ { - "dstack": "Dstack", - "common": { - "ok": "OK", - "loading": "Loading", - "add": "Add", - "yes": "Yes", - "no": "No", - "create": "Create", - "create_wit_text": "Create {{text}}", - "edit": "Edit", - "delete": "Delete", - "remove": "Remove", - "apply": "Apply", - "next": "Next", - "previous": "Back", - "settings": "Settings", - "match_count_with_value_one": "{{count}} match", - "match_count_with_value_other": "{{count}} matches", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "sign_out": "Sign out", - "cancel": "Cancel", - "save": "Save", - "send" : "Send", - "profile": "Profile", - "copied": "Copied", - "copy": "Copy", - "info": "Info", - "stop": "Stop", - "abort": "Abort", - "close": "Close", - "clearFilter": "Clear filter", - "server_error": "Server error: {{error}}", - "login": "Sign in", - "login_github": "Sign in with GitHub", - "login_okta": "Sign in with Okta", - "login_entra": "Sign in with EntraID", - "login_google": "Sign in with Google", - "general": "General", - "test": "Test", - "local_storage_unavailable": "Local Storage is unavailable", - "local_storage_unavailable_message": "Your browser doesn't support local storage", - "object": "Object", - "objects_other": "Objects", - "continue": "Continue", - "select_visible_columns": "Select visible columns", - "tutorial": "Tutorials", - "tutorial_other": "Take a tour", - "docs": "Docs", - "discord": "Discord", - "danger_zone": "Danger Zone", - "control_plane": "Control plane", - "refresh": "Refresh", - "quickstart": "Quickstart", - "ask_ai": "Ask AI", - "new": "New", - "full_view": "Full view" - }, - - "auth": { - "invalid_token": "Invalid token", - "you_are_not_logged_in": "You are not logged in", - "contact_to_administrator": "For getting the authorization token, contact to the administrator", - "sign_in_to_dstack": "Welcome to dstack Sky", - "sign_in_to_dstack_enterprise": "Welcome to dstack", - "authorization_failed": "Authorization is failed", - "try_again": "Please try again", - "login_by_token": "Sign in via a token", - "another_login_methods": "Other sign in options" - }, - - "navigation": { - "settings": "Settings", - "runs": "Runs", - "models": "Models", - "fleets": "Fleets", - "fleet": "Fleet", - "project": "project", - "project_other": "Projects", - "general": "General", - "users": "Users", - "user_settings": "User settings", - "account": "User", - "billing": "Billing", - "resources": "Resources", - "volumes": "Volumes", - "instances": "Instances", - "offers": "Offers", - "events": "Events" - }, - - "backend": { - "page_title_one": "Backend", - "page_title_other": "Backends", - "add_backend": "Add backend", - "edit_backend": "Edit backend", - "empty_message_title": "No backends", - "empty_message_text": "No backends to display.", - "type": { - "aws": "AWS", - "aws_description": "Run workflows and store data in Amazon Web Services ", - "gcp": "GCP", - "gcp_description": "Run workflows and store data in Google Cloud Platform", - "azure": "Azure", - "azure_description": "Run workflows and store data in Microsoft Azure", - "lambda": "Lambda", - "lambda_description": "Run workflows and store data in Lambda", - "local": "Local", - "local_description": "Run workflows and store data locally via Docker" + "dstack": "Dstack", + "common": { + "ok": "OK", + "loading": "Loading", + "add": "Add", + "yes": "Yes", + "no": "No", + "create": "Create", + "create_wit_text": "Create {{text}}", + "edit": "Edit", + "delete": "Delete", + "remove": "Remove", + "apply": "Apply", + "next": "Next", + "previous": "Back", + "settings": "Settings", + "match_count_with_value_one": "{{count}} match", + "match_count_with_value_other": "{{count}} matches", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "sign_out": "Sign out", + "cancel": "Cancel", + "save": "Save", + "send": "Send", + "profile": "Profile", + "copied": "Copied", + "copy": "Copy", + "info": "Info", + "stop": "Stop", + "abort": "Abort", + "close": "Close", + "clearFilter": "Clear filter", + "server_error": "Server error: {{error}}", + "login": "Sign in", + "login_github": "Sign in with GitHub", + "login_okta": "Sign in with Okta", + "login_entra": "Sign in with EntraID", + "login_google": "Sign in with Google", + "general": "General", + "test": "Test", + "local_storage_unavailable": "Local Storage is unavailable", + "local_storage_unavailable_message": "Your browser doesn't support local storage", + "object": "Object", + "objects_other": "Objects", + "continue": "Continue", + "select_visible_columns": "Select visible columns", + "tutorial": "Tutorials", + "tutorial_other": "Take a tour", + "docs": "Docs", + "discord": "Discord", + "danger_zone": "Danger Zone", + "control_plane": "Control plane", + "refresh": "Refresh", + "quickstart": "Quickstart", + "ask_ai": "Ask AI", + "new": "New", + "full_view": "Full view" }, - "table": { - "region": "Region", - "bucket": "Storage" + "auth": { + "invalid_token": "Invalid token", + "you_are_not_logged_in": "You are not logged in", + "contact_to_administrator": "For getting the authorization token, contact to the administrator", + "sign_in_to_dstack": "Welcome to dstack Sky", + "sign_in_to_dstack_enterprise": "Welcome to dstack", + "authorization_failed": "Authorization is failed", + "try_again": "Please try again", + "login_by_token": "Sign in via a token", + "another_login_methods": "Other sign in options" }, - "edit": { - "success_notification": "Project updating is successful", - "delete_backend_confirm_title": "Delete backend", - "delete_backend_confirm_message": "Are you sure you want to delete this backend?", - "delete_backends_confirm_title": "Delete backends", - "delete_backends_confirm_message": "Are you sure you want to delete these backends?" + "navigation": { + "settings": "Settings", + "runs": "Runs", + "models": "Models", + "fleets": "Fleets", + "fleet": "Fleet", + "project": "project", + "project_other": "Projects", + "general": "General", + "users": "Users", + "user_settings": "User settings", + "account": "User", + "billing": "Billing", + "resources": "Resources", + "volumes": "Volumes", + "instances": "Instances", + "offers": "Offers", + "events": "Events" }, - "create": { - "success_notification": "Backend is created" - } - }, + "backend": { + "page_title_one": "Backend", + "page_title_other": "Backends", + "add_backend": "Add backend", + "edit_backend": "Edit backend", + "empty_message_title": "No backends", + "empty_message_text": "No backends to display.", + "type": { + "aws": "AWS", + "aws_description": "Run workflows and store data in Amazon Web Services ", + "gcp": "GCP", + "gcp_description": "Run workflows and store data in Google Cloud Platform", + "azure": "Azure", + "azure_description": "Run workflows and store data in Microsoft Azure", + "lambda": "Lambda", + "lambda_description": "Run workflows and store data in Lambda", + "local": "Local", + "local_description": "Run workflows and store data locally via Docker" + }, - "gateway": { - "page_title_one": "Gateway", - "page_title_other": "Gateways", - "add_gateway": "Add gateway", - "edit_gateway": "Edit gateway", - "empty_message_title": "No gateways", - "empty_message_text": "No gateways to display.", + "table": { + "region": "Region", + "bucket": "Storage" + }, - "edit": { - "backend": "Backend", - "backend_description": "Select a backend", - "region": "Region", - "region_description": "Select a region", - "default": "Default", - "default_checkbox": "Turn on default", - "external_ip": "External IP", - "wildcard_domain": "Wildcard domain", - "wildcard_domain_description": "Specify the wildcard domain mapped to the external IP.", - "wildcard_domain_placeholder": "*.mydomain.com", - "delete_gateway_confirm_title": "Delete gateway", - "delete_gateway_confirm_message": "Are you sure you want to delete this gateway?", - "delete_gateways_confirm_title": "Delete gateways", - "delete_gateways_confirm_message": "Are you sure you want to delete these gateways?", + "edit": { + "success_notification": "Project updating is successful", + "delete_backend_confirm_title": "Delete backend", + "delete_backend_confirm_message": "Are you sure you want to delete this backend?", + "delete_backends_confirm_title": "Delete backends", + "delete_backends_confirm_message": "Are you sure you want to delete these backends?" + }, - "validation": { - "wildcard_domain_format": "Should use next format: {{pattern}}" - } + "create": { + "success_notification": "Backend is created" + } }, - "create": { - "success_notification": "Gateway is created", - "creating_notification": "The gateway is creating. It may take some time" - }, + "gateway": { + "page_title_one": "Gateway", + "page_title_other": "Gateways", + "add_gateway": "Add gateway", + "edit_gateway": "Edit gateway", + "empty_message_title": "No gateways", + "empty_message_text": "No gateways to display.", - "update": { - "success_notification": "Gateway is updated" - }, + "edit": { + "backend": "Backend", + "backend_description": "Select a backend", + "region": "Region", + "region_description": "Select a region", + "default": "Default", + "default_checkbox": "Turn on default", + "external_ip": "External IP", + "wildcard_domain": "Wildcard domain", + "wildcard_domain_description": "Specify the wildcard domain mapped to the external IP.", + "wildcard_domain_placeholder": "*.mydomain.com", + "delete_gateway_confirm_title": "Delete gateway", + "delete_gateway_confirm_message": "Are you sure you want to delete this gateway?", + "delete_gateways_confirm_title": "Delete gateways", + "delete_gateways_confirm_message": "Are you sure you want to delete these gateways?", - "test_domain": { - "success_notification": "Domain is valid" - } - }, + "validation": { + "wildcard_domain_format": "Should use next format: {{pattern}}" + } + }, - "projects": { - "page_title": "Projects", - "search_placeholder": "Find projects", - "empty_message_title": "No projects", - "empty_message_text": "No projects to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "nomatch_message_button_label": "Clear filter", - "repositories": "Repositories", - "runs": "Runs", - "tags": "Tags", - "events": "Events", - "settings": "Settings", - "join": "Join", - "leave_confirm_title": "Leave project", - "leave_confirm_message": "Are you sure you want to leave this project?", - "leave": "Leave", - "join_success": "Successfully joined the project", - "leave_success": "Successfully left the project", - "join_error": "Failed to join project", - "leave_error": "Failed to leave project", - "card": { - "backend": "Backend", - "settings": "Settings" - }, - "wizard": { - "submit": "Create" - }, - "edit": { - "general": "General", - "project_name": "Name", - "owner": "Owner", - "project_name_description": "Only latin characters, dashes, underscores, and digits", - "project_type": "Project type", - "project_type_description": "Choose which project type you want to create", - "backends": "Backends", - "base_backends_description": "dstack will automatically collect offers from the following providers. Deselect providers you don’t want to use.", - "backends_description": "The following backends can be configured with your own cloud credentials in the project settings after the project is created.", - "create_default_fleet": "Create a default fleet", - "default_fleet": "Default fleet", - "default_fleet_description": "At least one fleet is required to run dev environments, tasks, or services.", - "is_public": "Public", - "is_public_description": "Allow any user join the project as a member", - "backend": "Backend", - "backend_config": "Backend config", - "backend_config_description": "Specify the backend config in the YAML format. Click Info for examples.", - "backend_type": "Type", - "backend_type_description": "Select a backend type", - "members_empty_message_title": "No members", - "members_empty_message_text": "Select project's members", - "update_members_success": "Members are updated", - "update_visibility_success": "Project visibility updated successfully", - "update_visibility_confirm_title": "Change project visibility", - "update_visibility_confirm_message": "Are you sure you want to change the project visibility? This will affect who can access this project.", - "change_visibility": "Change", - "project_visibility": "Visibility", - "project_visibility_description": "Control who can access this project", - "make_project_public": "Make project public", - "delete_project_confirm_title": "Delete project", - "delete_project_confirm_message": "Are you sure you want to delete this project?", - "delete_projects_confirm_title": "Delete projects", - "delete_projects_confirm_message": "Are you sure you want to delete these projects?", - "delete_this_project": "Delete this project", - "cli": "CLI", - "aws": { - "authorization": "Authorization", - "authorization_default": "Default credentials", - "authorization_access_key": "Access key", - "access_key": "Access key", - "access_key_id": "Access key ID", - "access_key_id_description": "Specify the AWS access key ID", - "secret_key": "Secret key", - "secret_key_id": "Secret access key", - "secret_key_id_description": "Specify the AWS secret access key", - "regions": "Regions", - "regions_description": "Select regions to run workflows and store artifacts", - "regions_placeholder": "Select regions", - "s3_bucket_name": "Bucket", - "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", - "vpc_name": "VPC", - "vpc_name_description": "Enter a vpc" - }, - "azure" : { - "authorization": "Authorization", - "authorization_default": "Default credentials", - "authorization_client": "Client secret", - "tenant_id": "Tenant ID", - "tenant_id_description": "Specify an Azure tenant ID", - "tenant_id_placeholder": "Not selected", - "client_id": "Client ID", - "client_id_description": "Specify an Azure client (application) ID", - "client_secret": "Client secret", - "client_secret_description": "Specify an Azure client (application) secret", - "subscription_id": "Subscription ID", - "subscription_id_description": "Select an Azure subscription ID", - "subscription_id_placeholder": "Not selected", - "locations": "Locations", - "locations_description": "Select locations to run workflows", - "locations_placeholder": "Select locations", - "storage_account": "Storage account", - "storage_account_description": "Select an Azure storage account to store artifacts", - "storage_account_placeholder": "Not selected" + "create": { + "success_notification": "Gateway is created", + "creating_notification": "The gateway is creating. It may take some time" + }, - }, - "gcp": { - "authorization": "Authorization", - "authorization_default": "Default credentials", - "service_account": "Service account key", - "credentials_description": "Credentials description", - "credentials_placeholder": "Credentials placeholder", - "regions": "Regions", - "regions_description": "Select regions to run workflows and store artifacts", - "regions_placeholder": "Select regions", - "project_id": "Project Id", - "project_id_description": "Select a project id", - "project_id_placeholder": "Select a project Id" - }, - "lambda": { - "api_key": "API key", - "api_key_description": "Specify the Lambda API key", - "regions": "Regions", - "regions_description": "Select regions to run workflows", - "regions_placeholder": "Select regions", - "storage_backend": { - "type": "Storage", - "type_description": "Select backend storage", - "type_placeholder": "Select type", - "credentials": { - "access_key_id": "Access key ID", - "access_key_id_description": "Specify the AWS access key ID", - "secret_key_id": "Secret access key", - "secret_key_id_description": "Specify the AWS secret access key" - }, - "s3_bucket_name": "Bucket", - "s3_bucket_name_description": "Select an S3 bucket to store artifacts" - } - }, - "local": { - "path": "Files path" - }, - "members": { - "section_title": "Members", - "name": "User name", - "role": "Project role" - }, - "secrets": { - "section_title": "Secrets", - "empty_message_title": "No secrets", - "empty_message_text": "No secrets to display.", - "name": "Secret name", - "value": "Secret value", - "create_secret": "Create secret", - "update_secret": "Update secret", - "delete_confirm_title": "Delete secret", - "delete_confirm_message": "Are you sure you want to delete the {{name}} secret?", - "multiple_delete_confirm_title": "Delete secrets", - "multiple_delete_confirm_message": "Are you sure you want to delete {{count}} secrets?", - "not_permissions_title": "No permissions", - "not_permissions_description": "You don't have permissions for managing secrets", - "validation": { - "secret_name_format": "Invalid secret name" + "update": { + "success_notification": "Gateway is updated" + }, + + "test_domain": { + "success_notification": "Domain is valid" } - }, - "error_notification": "Update project error", - "validation": { - "user_name_format": "Only letters, numbers, - or _" - }, - "visibility": { - "private": "Private", - "public": "Public" - } - }, - "create": { - "page_title": "Create project", - "error_notification": "Create project error", - "success_notification": "Project is created" }, - "repo": { - "search_placeholder": "Find repositories", - "empty_message_title": "No repositories", - "empty_message_text": "No repositories to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "card": { - "owner": "Owner", - "last_run": "Last run", - "tags_count": "Tags count", - "directory": "Directory" - }, - "secrets": { - "table_title": "Secrets", - "add_modal_title": "Add secret", - "update_modal_title": "Update secret", - "name": "Secret name", - "name_description": "Secret name", - "value": "Secret value", - "value_description": "Secret value", - "search_placeholder": "Find secrets", - "empty_message_title": "No secrets", - "empty_message_text": "No secrets to display." - } + + "projects": { + "page_title": "Projects", + "search_placeholder": "Find projects", + "empty_message_title": "No projects", + "empty_message_text": "No projects to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "nomatch_message_button_label": "Clear filter", + "repositories": "Repositories", + "runs": "Runs", + "tags": "Tags", + "events": "Events", + "settings": "Settings", + "join": "Join", + "leave_confirm_title": "Leave project", + "leave_confirm_message": "Are you sure you want to leave this project?", + "leave": "Leave", + "join_success": "Successfully joined the project", + "leave_success": "Successfully left the project", + "join_error": "Failed to join project", + "leave_error": "Failed to leave project", + "card": { + "backend": "Backend", + "settings": "Settings" + }, + "wizard": { + "submit": "Create" + }, + "edit": { + "general": "General", + "project_name": "Name", + "owner": "Owner", + "project_name_description": "Only latin characters, dashes, underscores, and digits", + "project_type": "Project type", + "project_type_description": "Choose which project type you want to create", + "backends": "Backends", + "base_backends_description": "dstack will automatically collect offers from the following providers. Deselect providers you don’t want to use.", + "backends_description": "The following backends can be configured with your own cloud credentials in the project settings after the project is created.", + "create_default_fleet": "Create a default fleet", + "default_fleet": "Default fleet", + "default_fleet_description": "At least one fleet is required to run dev environments, tasks, or services.", + "is_public": "Public", + "is_public_description": "Allow any user join the project as a member", + "backend": "Backend", + "backend_config": "Backend config", + "backend_config_description": "Specify the backend config in the YAML format. Click Info for examples.", + "backend_type": "Type", + "backend_type_description": "Select a backend type", + "members_empty_message_title": "No members", + "members_empty_message_text": "Select project's members", + "update_members_success": "Members are updated", + "update_visibility_success": "Project visibility updated successfully", + "update_visibility_confirm_title": "Change project visibility", + "update_visibility_confirm_message": "Are you sure you want to change the project visibility? This will affect who can access this project.", + "change_visibility": "Change", + "project_visibility": "Visibility", + "project_visibility_description": "Control who can access this project", + "make_project_public": "Make project public", + "delete_project_confirm_title": "Delete project", + "delete_project_confirm_message": "Are you sure you want to delete this project?", + "delete_projects_confirm_title": "Delete projects", + "delete_projects_confirm_message": "Are you sure you want to delete these projects?", + "delete_this_project": "Delete this project", + "cli": "CLI", + "aws": { + "authorization": "Authorization", + "authorization_default": "Default credentials", + "authorization_access_key": "Access key", + "access_key": "Access key", + "access_key_id": "Access key ID", + "access_key_id_description": "Specify the AWS access key ID", + "secret_key": "Secret key", + "secret_key_id": "Secret access key", + "secret_key_id_description": "Specify the AWS secret access key", + "regions": "Regions", + "regions_description": "Select regions to run workflows and store artifacts", + "regions_placeholder": "Select regions", + "s3_bucket_name": "Bucket", + "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", + "vpc_name": "VPC", + "vpc_name_description": "Enter a vpc" + }, + "azure": { + "authorization": "Authorization", + "authorization_default": "Default credentials", + "authorization_client": "Client secret", + "tenant_id": "Tenant ID", + "tenant_id_description": "Specify an Azure tenant ID", + "tenant_id_placeholder": "Not selected", + "client_id": "Client ID", + "client_id_description": "Specify an Azure client (application) ID", + "client_secret": "Client secret", + "client_secret_description": "Specify an Azure client (application) secret", + "subscription_id": "Subscription ID", + "subscription_id_description": "Select an Azure subscription ID", + "subscription_id_placeholder": "Not selected", + "locations": "Locations", + "locations_description": "Select locations to run workflows", + "locations_placeholder": "Select locations", + "storage_account": "Storage account", + "storage_account_description": "Select an Azure storage account to store artifacts", + "storage_account_placeholder": "Not selected" + }, + "gcp": { + "authorization": "Authorization", + "authorization_default": "Default credentials", + "service_account": "Service account key", + "credentials_description": "Credentials description", + "credentials_placeholder": "Credentials placeholder", + "regions": "Regions", + "regions_description": "Select regions to run workflows and store artifacts", + "regions_placeholder": "Select regions", + "project_id": "Project Id", + "project_id_description": "Select a project id", + "project_id_placeholder": "Select a project Id" + }, + "lambda": { + "api_key": "API key", + "api_key_description": "Specify the Lambda API key", + "regions": "Regions", + "regions_description": "Select regions to run workflows", + "regions_placeholder": "Select regions", + "storage_backend": { + "type": "Storage", + "type_description": "Select backend storage", + "type_placeholder": "Select type", + "credentials": { + "access_key_id": "Access key ID", + "access_key_id_description": "Specify the AWS access key ID", + "secret_key_id": "Secret access key", + "secret_key_id_description": "Specify the AWS secret access key" + }, + "s3_bucket_name": "Bucket", + "s3_bucket_name_description": "Select an S3 bucket to store artifacts" + } + }, + "local": { + "path": "Files path" + }, + "members": { + "section_title": "Members", + "name": "User name", + "role": "Project role" + }, + "secrets": { + "section_title": "Secrets", + "empty_message_title": "No secrets", + "empty_message_text": "No secrets to display.", + "name": "Secret name", + "value": "Secret value", + "create_secret": "Create secret", + "update_secret": "Update secret", + "delete_confirm_title": "Delete secret", + "delete_confirm_message": "Are you sure you want to delete the {{name}} secret?", + "multiple_delete_confirm_title": "Delete secrets", + "multiple_delete_confirm_message": "Are you sure you want to delete {{count}} secrets?", + "not_permissions_title": "No permissions", + "not_permissions_description": "You don't have permissions for managing secrets", + "validation": { + "secret_name_format": "Invalid secret name" + } + }, + "error_notification": "Update project error", + "validation": { + "user_name_format": "Only letters, numbers, - or _" + }, + "visibility": { + "private": "Private", + "public": "Public" + } + }, + "create": { + "page_title": "Create project", + "error_notification": "Create project error", + "success_notification": "Project is created" + }, + "repo": { + "search_placeholder": "Find repositories", + "empty_message_title": "No repositories", + "empty_message_text": "No repositories to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "card": { + "owner": "Owner", + "last_run": "Last run", + "tags_count": "Tags count", + "directory": "Directory" + }, + "secrets": { + "table_title": "Secrets", + "add_modal_title": "Add secret", + "update_modal_title": "Update secret", + "name": "Secret name", + "name_description": "Secret name", + "value": "Secret value", + "value_description": "Secret value", + "search_placeholder": "Find secrets", + "empty_message_title": "No secrets", + "empty_message_text": "No secrets to display." + } + }, + "run": { + "list_page_title": "Runs", + "search_placeholder": "Find runs", + "empty_message_title": "No runs", + "empty_message_text": "No runs to display.", + "quickstart_message_text": "Check out the quickstart guide to get started with dstack", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match. Try to change project or clear filter", + "filter_property_placeholder": "Filter by properties", + "project": "Project", + "project_placeholder": "Filtering by project", + "repo": "Repository", + "repo_placeholder": "Filtering by repository", + "user": "User", + "user_placeholder": "Filtering by user", + "active_only": "Active runs", + "log": "Logs", + "log_empty_message_title": "No logs", + "log_empty_message_text": "No logs to display.", + "inspect": "Inspect", + "run_name": "Name", + "workflow_name": "Workflow", + "configuration": "Configuration", + "instance": "Instance", + "priority": "Priority", + "provider_name": "Provider", + "status": "Status", + "probe": "Probes", + "submitted_at": "Submitted", + "finished_at": "Finished", + "metrics": { + "title": "Metrics", + "show_metrics": "Show metrics", + "cpu_utilization": "CPU utilization %", + "memory_used": "System memory used", + "per_each_cpu_utilization": "GPU utilization %", + "per_each_memory_used": "GPU memory used" + }, + "jobs": "Jobs", + "job_name": "Job Name", + "cost": "Cost", + "backend": "Backend", + "region": "Region", + "instance_id": "Instance ID", + "schedule": "Schedule", + "next_run": "Next run", + "resources": "Resources", + "spot": "Spot", + "termination_reason": "Termination reason", + "price": "Price", + "error": "Error", + "artifacts": "Artifacts", + "artifacts_count": "Artifacts", + "hub_user_name": "User", + "service_url": "Service URL", + "statuses": { + "pending": "Pending", + "submitted": "Submitted", + "provisioning": "Provisioning", + "pulling": "Pulling", + "downloading": "Downloading", + "running": "Running", + "uploading": "Uploading", + "stopping": "Stopping", + "stopped": "Stopped", + "terminating": "Terminating", + "terminated": "Terminated", + "aborting": "Aborting", + "aborted": "Aborted", + "failed": "Failed", + "done": "Done", + "building": "Building" + } + }, + "tag": { + "list_page_title": "Artifacts", + "search_placeholder": "Find tags", + "empty_message_title": "No tags", + "empty_message_text": "No tags to display.", + "tag_name": "Tag", + "run_name": "Run", + "artifacts": "Files" + }, + "artifact": { + "list_page_title": "Artifacts", + "search_placeholder": "Find objects", + "empty_message_title": "No objects", + "empty_message_text": "No objects to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "name": "Name", + "type": "Type", + "size": "Size" + } }, - "run": { - "list_page_title": "Runs", - "search_placeholder": "Find runs", - "empty_message_title": "No runs", - "empty_message_text": "No runs to display.", - "quickstart_message_text": "Check out the quickstart guide to get started with dstack", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match. Try to change project or clear filter", - "filter_property_placeholder": "Filter by properties", - "project": "Project", - "project_placeholder": "Filtering by project", - "repo": "Repository", - "repo_placeholder": "Filtering by repository", - "user": "User", - "user_placeholder": "Filtering by user", - "active_only": "Active runs", - "log": "Logs", - "log_empty_message_title": "No logs", - "log_empty_message_text": "No logs to display.", - "inspect": "Inspect", - "run_name": "Name", - "workflow_name": "Workflow", - "configuration": "Configuration", - "instance": "Instance", - "priority": "Priority", - "provider_name": "Provider", - "status": "Status", - "probe": "Probes", - "submitted_at": "Submitted", - "finished_at": "Finished", - "metrics": { - "title": "Metrics", - "show_metrics": "Show metrics", - "cpu_utilization": "CPU utilization %", - "memory_used": "System memory used", - "per_each_cpu_utilization": "GPU utilization %", - "per_each_memory_used": "GPU memory used" - }, - "jobs": "Jobs", - "job_name": "Job Name", - "cost": "Cost", - "backend": "Backend", - "region": "Region", - "instance_id": "Instance ID", - "schedule": "Schedule", - "next_run": "Next run", - "resources": "Resources", - "spot": "Spot", - "termination_reason": "Termination reason", - "price": "Price", - "error": "Error", - "artifacts": "Artifacts", - "artifacts_count": "Artifacts", - "hub_user_name": "User", - "service_url": "Service URL", - "statuses": { - "pending": "Pending", - "submitted": "Submitted", - "provisioning": "Provisioning", - "pulling": "Pulling", - "downloading": "Downloading", - "running": "Running", - "uploading": "Uploading", - "stopping": "Stopping", - "stopped": "Stopped", - "terminating": "Terminating", - "terminated": "Terminated", - "aborting": "Aborting", - "aborted": "Aborted", - "failed": "Failed", - "done": "Done", - "building": "Building" - } + "runs": { + "dev_env": { + "wizard": { + "title": "New dev environment", + "submit": "Apply", + "offer": "Offer", + "offer_description": "Select an offer for the dev environment.", + "env_type": "Env_TYPE", + "env_type_description": "Env_TYPE_DESCRIPTION", + "env_type_constraint": "Env_TYPE_CONSTRAINT", + "env_type_placeholder": "Env_TYPE_PLACEHOLDER", + "password": "Password", + "password_description": "Coder password", + "password_placeholder": "Enter a password", + "name": "Name", + "name_description": "The name of the run, e.g. 'my-dev-env'", + "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", + "name_placeholder": "Optional", + "ide": "IDE", + "ide_description": "Select which IDE would you like to use with the dev environment.", + "docker": "Docker", + "docker_image": "Image", + "docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'", + "docker_image_constraint": "The image must be public", + "docker_image_placeholder": "Required", + "python": "Python", + "python_description": "The version of Python, e.g. '3.12'", + "python_placeholder": "Optional", + "repo": "Repo", + "working_dir": "Working dir", + "working_dir_description": "The absolute path to the working directory inside the container, e.g. '/home/user/project'", + "working_dir_placeholder": "Optional", + "working_dir_constraint": "By default, set to '/workflow'", + "repo_url": "URL", + "repo_url_description": "A URL of a Git repository, e.g. 'https://github.com/user/repo'", + "repo_url_constraint": "The repo must be public", + "repo_url_placeholder": "Required", + "repo_path": "Path", + "repo_path_description": "The path inside the container to clone the repository, e.g. '/home/user/project'", + "repo_path_placeholder": "Optional", + "repo_path_constraint": "By default, set to '/workflow'", + "config": "Configuration file", + "config_description": "Review the configuration file and adjust it if needed. Click Info for examples.", + "success_notification": "The run is submitted!" + } + } }, - "tag": { - "list_page_title": "Artifacts", - "search_placeholder": "Find tags", - "empty_message_title": "No tags", - "empty_message_text": "No tags to display.", - "tag_name": "Tag", - "run_name": "Run", - "artifacts": "Files" + "offer": { + "title": "Offers", + "filter_property_placeholder": "Filter by properties", + "backend": "Backend", + "backend_plural": "Backends", + "availability": "Availability", + "groupBy": "Group by properties", + "region": "Region", + "count": "Count", + "price": "$/GPU", + "memory_mib": "Memory", + "spot": "Spot policy", + "empty_message_title_select_project": "Select a project", + "empty_message_text_select_project": "Use the filter above to select a project", + "empty_message_title_select_groupBy": "Select a group by", + "empty_message_text_select_groupBy": "Use the field above to select a group by", + "empty_message_title": "No offers", + "empty_message_text": "No offers to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match." }, - "artifact": { - "list_page_title": "Artifacts", - "search_placeholder": "Find objects", - "empty_message_title": "No objects", - "empty_message_text": "No objects to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "name": "Name", - "type": "Type", - "size": "Size" - } - }, - "runs": { - "dev_env": { - "wizard": { - "title": "New dev environment", - "submit": "Apply", - "offer": "Offer", - "offer_description": "Select an offer for the dev environment.", - "name": "Name", - "name_description": "The name of the run, e.g. 'my-dev-env'", - "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", - "name_placeholder": "Optional", - "ide": "IDE", - "ide_description": "Select which IDE would you like to use with the dev environment.", - "docker": "Docker", - "docker_image": "Image", - "docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'", - "docker_image_constraint": "The image must be public", - "docker_image_placeholder": "Required", - "python": "Python", - "python_description": "The version of Python, e.g. '3.12'", - "python_placeholder": "Optional", - "repo": "Repo", - "working_dir": "Working dir", - "working_dir_description": "The absolute path to the working directory inside the container, e.g. '/home/user/project'", - "working_dir_placeholder": "Optional", - "working_dir_constraint": "By default, set to '/workflow'", - "repo_url": "URL", - "repo_url_description": "A URL of a Git repository, e.g. 'https://github.com/user/repo'", - "repo_url_constraint": "The repo must be public", - "repo_url_placeholder": "Required", - "repo_path": "Path", - "repo_path_description": "The path inside the container to clone the repository, e.g. '/home/user/project'", - "repo_path_placeholder": "Optional", - "repo_path_constraint": "By default, set to '/workflow'", - "config": "Configuration file", - "config_description": "Review the configuration file and adjust it if needed. Click Info for examples.", - "success_notification": "The run is submitted!" - } - } - }, - "offer": { - "title": "Offers", - "filter_property_placeholder": "Filter by properties", - "backend": "Backend", - "backend_plural": "Backends", - "availability": "Availability", - "groupBy": "Group by properties", - "region": "Region", - "count": "Count", - "price": "$/GPU", - "memory_mib": "Memory", - "spot": "Spot policy", - "empty_message_title_select_project": "Select a project", - "empty_message_text_select_project": "Use the filter above to select a project", - "empty_message_title_select_groupBy": "Select a group by", - "empty_message_text_select_groupBy": "Use the field above to select a group by", - "empty_message_title": "No offers", - "empty_message_text": "No offers to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match." - }, - "models": { - "model_name": "Name", - "url": "URL", - "gateway": "Gateway", - "type": "Type", - "run": "Run", - "resources": "Resources", - "price": "Price", - "submitted_at": "Submitted", - "user": "User", - "repository": "Repository", - "backend": "Backend", - "code": "Code", - "empty_message_title": "No models", - "empty_message_text": "No models to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "nomatch_message_button_label": "Clear filter", + "models": { + "model_name": "Name", + "url": "URL", + "gateway": "Gateway", + "type": "Type", + "run": "Run", + "resources": "Resources", + "price": "Price", + "submitted_at": "Submitted", + "user": "User", + "repository": "Repository", + "backend": "Backend", + "code": "Code", + "empty_message_title": "No models", + "empty_message_text": "No models to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "nomatch_message_button_label": "Clear filter", - "details": { - "instructions": "System", - "instructions_description": "Specify system", - "message_placeholder": "Enter your question", - "chat_empty_title": "No messages yet", - "chat_empty_message": "Please start a chat", - "run_name": "Run name", - "view_code": "View code", - "view_code_description": "You can use the following code to start integrating your current prompt and settings into your application." - } - }, - - "fleets": { - "no_alert": { - "title": "No fleets", - "description": "The project has no fleets. Create one before submitting a run.", - "button_title": "Create a fleet" - }, - "fleet": "Fleet", - "fleet_placeholder": "Filtering by fleet", - "fleet_name": "Fleet name", - "total_instances": "Number of instances", - "inspect": "Inspect", - "empty_message_title": "No fleets", - "empty_message_text": "No fleets to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "nomatch_message_button_label": "Clear filter", - "active_only": "Active fleets", - "filter_property_placeholder": "Filter by properties", - "statuses": { - "active": "Active", - "submitted": "Submitted", - "failed": "Failed", - "terminating": "Terminating", - "terminated": "Terminated" + "details": { + "instructions": "System", + "instructions_description": "Specify system", + "message_placeholder": "Enter your question", + "chat_empty_title": "No messages yet", + "chat_empty_message": "Please start a chat", + "run_name": "Run name", + "view_code": "View code", + "view_code_description": "You can use the following code to start integrating your current prompt and settings into your application." + } }, - "create": { - "success_notification": "The fleet is created!" + + "fleets": { + "no_alert": { + "title": "No fleets", + "description": "The project has no fleets. Create one before submitting a run.", + "button_title": "Create a fleet" + }, + "fleet": "Fleet", + "fleet_placeholder": "Filtering by fleet", + "fleet_name": "Fleet name", + "total_instances": "Number of instances", + "inspect": "Inspect", + "empty_message_title": "No fleets", + "empty_message_text": "No fleets to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "nomatch_message_button_label": "Clear filter", + "active_only": "Active fleets", + "filter_property_placeholder": "Filter by properties", + "statuses": { + "active": "Active", + "submitted": "Submitted", + "failed": "Failed", + "terminating": "Terminating", + "terminated": "Terminated" + }, + "create": { + "success_notification": "The fleet is created!" + }, + "instances": { + "active_only": "Active instances", + "filter_property_placeholder": "Filter by properties", + "title": "Instances", + "empty_message_title": "No instances", + "empty_message_text": "No instances to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "instance_name": "Instance", + "instance_num": "Instance num", + "created": "Created", + "status": "Status", + "project": "Project", + "hostname": "Host name", + "instance_type": "Type", + "statuses": { + "pending": "Pending", + "provisioning": "Provisioning", + "idle": "Idle", + "busy": "Busy", + "terminating": "Terminating", + "terminated": "Terminated" + }, + "resources": "Resources", + "backend": "Backend", + "region": "Region", + "spot": "Spot", + "started": "Started", + "price": "Price" + }, + "edit": { + "name": "Name", + "name_description": "The name of the fleet, e.g. 'my-fleet'", + "name_placeholder": "Optional", + "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", + "min_instances": "Min number of instances", + "min_instances_description": "Set it '0' to provision instances only when required", + "max_instances": "Max number of instances", + "max_instances_description": "Required only if you want to set an upper limit", + "max_instances_placeholder": "Optional", + "idle_duration": "Idle duration", + "idle_duration_description": "Example: '0s', '1m', '1h'", + "spot_policy": "Spot policy", + "spot_policy_description": "Set it to 'auto' to allow the use of both on-demand and spot instances" + } }, - "instances": { - "active_only": "Active instances", - "filter_property_placeholder": "Filter by properties", - "title": "Instances", - "empty_message_title": "No instances", - "empty_message_text": "No instances to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "instance_name": "Instance", - "instance_num": "Instance num", - "created": "Created", - "status": "Status", - "project": "Project", - "hostname": "Host name", - "instance_type": "Type", - "statuses": { - "pending": "Pending", - "provisioning": "Provisioning", - "idle": "Idle", - "busy": "Busy", - "terminating": "Terminating", - "terminated": "Terminated" - }, - "resources": "Resources", - "backend": "Backend", - "region": "Region", - "spot": "Spot", - "started": "Started", - "price": "Price" + "volume": { + "volumes": "Volumes", + "empty_message_title": "No volumes", + "empty_message_text": "No volumes to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "delete_volumes_confirm_title": "Delete volumes", + "delete_volumes_confirm_message": "Are you sure you want to delete these volumes?", + "active_only": "Active volumes", + "filter_property_placeholder": "Filter by properties", + + "name": "Name", + "project": "Project name", + "region": "Region", + "backend": "Backend", + "status": "Status", + "created": "Created", + "finished": "Finished", + "price": "Price (per month)", + "cost": "Cost", + "statuses": { + "failed": "Failed", + "submitted": "Submitted", + "provisioning": "Provisioning", + "active": "Active", + "deleted": "Deleted" + } }, - "edit": { - "name": "Name", - "name_description": "The name of the fleet, e.g. 'my-fleet'", - "name_placeholder": "Optional", - "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", - "min_instances": "Min number of instances", - "min_instances_description": "Set it '0' to provision instances only when required", - "max_instances": "Max number of instances", - "max_instances_description": "Required only if you want to set an upper limit", - "max_instances_placeholder": "Optional", - "idle_duration": "Idle duration", - "idle_duration_description": "Example: '0s', '1m', '1h'", - "spot_policy": "Spot policy", - "spot_policy_description": "Set it to 'auto' to allow the use of both on-demand and spot instances" - } - }, - "volume": { - "volumes": "Volumes", - "empty_message_title": "No volumes", - "empty_message_text": "No volumes to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "delete_volumes_confirm_title": "Delete volumes", - "delete_volumes_confirm_message": "Are you sure you want to delete these volumes?", - "active_only": "Active volumes", - "filter_property_placeholder": "Filter by properties", - "name": "Name", - "project": "Project name", - "region": "Region", - "backend": "Backend", - "status": "Status", - "created": "Created", - "finished": "Finished", - "price": "Price (per month)", - "cost": "Cost", - "statuses": { - "failed": "Failed", - "submitted": "Submitted", - "provisioning": "Provisioning", - "active": "Active", - "deleted": "Deleted" - } - }, + "events": { + "recorded_at": "Recorded At", + "actor": "Actor", + "targets": "Targets", + "message": "Message" + }, - "events": { - "recorded_at": "Recorded At", - "actor": "Actor", - "targets": "Targets", - "message": "Message" - }, + "users": { + "page_title": "Users", + "search_placeholder": "Find members", + "empty_message_title": "No members", + "empty_message_text": "No members to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match.", + "user_name": "User name", + "user_name_description": "Only latin characters, dashes, underscores, and digits", + "global_role_description": "Whether the user is an administrator or not", + "email_description": "Enter user email", + "token": "Token", + "token_description": "Specify use your personal access token", + "global_role": "Global role", + "active": "Active", + "active_description": "Specify user activation", + "activated": "Activated", + "deactivated": "Deactivated", + "email": "Email", + "created_at": "Created at", + "account": "User", + "account_settings": "User settings", + "settings": "Settings", + "projects": "Projects", + "events": "Events", + "create": { + "page_title": "Create user", + "error_notification": "Create user error", + "success_notification": "User is created" + }, + "edit": { + "error_notification": "Update user error", + "success_notification": "User updating is successful", + "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": "Are you sure you want to rotate token?", + "refresh_token_button_label": "Rotate", + "validation": { + "user_name_format": "Only letters, numbers, - or _", + "email_format": "Incorrect email" + } + }, - "users": { - "page_title": "Users", - "search_placeholder": "Find members", - "empty_message_title": "No members", - "empty_message_text": "No members to display.", - "nomatch_message_title": "No matches", - "nomatch_message_text": "We can't find a match.", - "user_name": "User name", - "user_name_description": "Only latin characters, dashes, underscores, and digits", - "global_role_description": "Whether the user is an administrator or not", - "email_description": "Enter user email", - "token": "Token", - "token_description": "Specify use your personal access token", - "global_role": "Global role", - "active": "Active", - "active_description": "Specify user activation", - "activated": "Activated", - "deactivated": "Deactivated", - "email": "Email", - "created_at": "Created at", - "account": "User", - "account_settings": "User settings", - "settings": "Settings", - "projects": "Projects", - "events": "Events", - "create": { - "page_title": "Create user", - "error_notification": "Create user error", - "success_notification": "User is created" - }, - "edit": { - "error_notification": "Update user error", - "success_notification": "User updating is successful", - "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": "Are you sure you want to rotate token?", - "refresh_token_button_label": "Rotate", - "validation": { - "user_name_format": "Only letters, numbers, - or _", - "email_format": "Incorrect email" - } - }, + "manual_payments": { + "title": "Credits history", + "add_payment": "Add payment", + "empty_message_title": "No payments", + "empty_message_text": "No payments to display.", - "manual_payments": { - "title": "Credits history", - "add_payment": "Add payment", - "empty_message_title": "No payments", - "empty_message_text": "No payments to display.", + "create": { + "success_notification": "Payment creating is successful" + }, - "create": { - "success_notification": "Payment creating is successful" - }, + "edit": { + "value": "Amount", + "value_description": "Enter amount here", + "description": "Description", + "description_description": "Describe payment here", + "created_at": "Created at" + } + }, - "edit": { - "value": "Amount", - "value_description": "Enter amount here", - "description": "Description", - "description_description": "Describe payment here", - "created_at": "Created at" - } + "token_copied": "Token copied" }, - - "token_copied": "Token copied" - }, - "billing": { - "title": "Billing", - "balance": "Balance", - "billing_history": "Billing history", - "payment_method": "Payment method", - "no_payment_method": "No payment method attached", - "top_up_balance": "Top up balance", - "edit_payment_method": "Edit payment method", - "payment_amount": "Payment amount", - "amount_description": "Minimum: ${{value}}", - "make_payment": "Make a payment", - "min_amount_error_message": "The amount is allowed to be more than {{value}}", - "payment_success_message": "Payment succeeded. There can be a short delay before the balance is updated." - }, - "validation": { - "required": "This is required field" - }, - "users_autosuggest": { - "placeholder": "Enter username or email to add member", - "entered_text": "Add member", - "loading": "Loading users", - "no_match": "No matches found" - }, - "roles": { - "admin": "Admin", - "manager": "Manager", - "user": "User" - }, - "confirm_dialog": { - "title": "Confirm delete", - "message": "Are you sure you want to delete?" - } + "billing": { + "title": "Billing", + "balance": "Balance", + "billing_history": "Billing history", + "payment_method": "Payment method", + "no_payment_method": "No payment method attached", + "top_up_balance": "Top up balance", + "edit_payment_method": "Edit payment method", + "payment_amount": "Payment amount", + "amount_description": "Minimum: ${{value}}", + "make_payment": "Make a payment", + "min_amount_error_message": "The amount is allowed to be more than {{value}}", + "payment_success_message": "Payment succeeded. There can be a short delay before the balance is updated." + }, + "validation": { + "required": "This is required field" + }, + "users_autosuggest": { + "placeholder": "Enter username or email to add member", + "entered_text": "Add member", + "loading": "Loading users", + "no_match": "No matches found" + }, + "roles": { + "admin": "Admin", + "manager": "Manager", + "user": "User" + }, + "confirm_dialog": { + "title": "Confirm delete", + "message": "Are you sure you want to delete?" + } } diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx index 98955d6a5..7c08b10b1 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx @@ -12,8 +12,10 @@ export const CONFIG_INFO = { export const FORM_FIELD_NAMES = { offer: 'offer', + env_type: 'offer', name: 'name', ide: 'ide', + password: 'password', config_yaml: 'config_yaml', docker: 'docker', image: 'image', @@ -24,6 +26,17 @@ export const FORM_FIELD_NAMES = { working_dir: 'working_dir', } as const satisfies Record; +export const ENV_TYPE_OPTIONS = [ + { + label: 'Web', + value: 'web', + }, + { + label: 'Desktop', + value: 'desktop', + }, +] as const; + export const IDE_OPTIONS = [ { label: 'Cursor', @@ -39,6 +52,11 @@ export const IDE_OPTIONS = [ }, ] as const; +export const IDE_CODER_OPTION = { + label: 'Coder', + value: 'coder', +} as const; + export const IDE_DISPLAY_NAMES: Record = { cursor: 'Cursor', vscode: 'VS Code', diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts index a693985fc..9f58838f8 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts @@ -15,10 +15,10 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { return ''; } - const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir } = formValues; + const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir, env_type, password } = formValues; - return jsYaml.dump({ - type: 'dev-environment', + let baseJson = { + type: env_type === 'web' ? 'service' : 'dev-environment', ...(name ? { name } : {}), ide, ...(docker ? { docker } : {}), @@ -38,10 +38,32 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { ...(working_dir ? { working_dir } : {}), backends: offer.backends, spot_policy: 'auto', - }); + }; + + if (env_type === 'web') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ide, ...props } = baseJson; + + baseJson = { + ...props, + + auth: false, + env: [`PASSWORD=${password}`, 'BIND_ADDR=0.0.0.0:8080'], + commands: [ + 'curl -fsSL https://code-server.dev/install.sh | sh -s -- --method standalone --prefix /tmp/code-server', + '/tmp/code-server/bin/code-server --bind-addr $BIND_ADDR --auth password --disable-telemetry --disable-update-check .', + ], + port: 8080, + gateway: true, + }; + } + + return jsYaml.dump(baseJson); }, [ formValues.name, + formValues.env_type, formValues.ide, + formValues.password, formValues.offer, formValues.python, formValues.image, diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts index 7815d1dac..461a0a18c 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts @@ -2,13 +2,13 @@ import { useCallback } from 'react'; import jsYaml from 'js-yaml'; import { useNotifications } from 'hooks'; +import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, getRepoUrlWithOutDir, slugify } from 'libs/repo'; import { useInitRepoMutation, useLazyGetRepoQuery } from 'services/repo'; -import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, getRepoUrlWithOutDir, slugify } from '../../../../libs/repo'; import { getRunSpecConfigurationResources } from '../helpers/getRunSpecConfigurationResources'; // TODO add next fields: volumes, repos, -const supportedFields: (keyof TDevEnvironmentConfiguration)[] = [ +const supportedFields: (keyof TDevEnvironmentConfiguration | keyof TServiceConfiguration)[] = [ 'type', 'init', 'inactivity_duration', @@ -33,6 +33,10 @@ const supportedFields: (keyof TDevEnvironmentConfiguration)[] = [ 'utilization_policy', 'fleets', 'repos', + 'auth', + 'commands', + 'port', + 'gateway', ]; export const useGetRunSpecFromYaml = ({ projectName = '' }) => { diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index fcac5194a..adf300246 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; @@ -7,13 +7,15 @@ import * as yup from 'yup'; import { Box, Link, WizardProps } from '@cloudscape-design/components'; import { CardsProps } from '@cloudscape-design/components/cards'; -import { TabsProps, ToggleProps } from 'components'; +import { Button, FormSelectProps, Popover, StatusIndicator, TabsProps, ToggleProps } from 'components'; import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Tabs, Toggle, Wizard } from 'components'; import { useBreadcrumbs, useNotifications } from 'hooks'; import { useCheckingForFleetsInProjects } from 'hooks/useCheckingForFleetsInProjectsOfMember'; -import { getServerError } from 'libs'; +import { copyToClipboard, getServerError } from 'libs'; +import { generateSecurePassword } from 'libs/password'; import { ROUTES } from 'routes'; +import { useGetProjectGatewaysQuery } from 'services/gateway'; import { useApplyRunMutation } from 'services/run'; import { OfferList } from 'pages/Offers/List'; @@ -21,7 +23,7 @@ import { NoFleetProjectAlert } from 'pages/Project/components/NoFleetProjectAler import { useGenerateYaml } from './hooks/useGenerateYaml'; import { useGetRunSpecFromYaml } from './hooks/useGetRunSpecFromYaml'; -import { FORM_FIELD_NAMES, IDE_OPTIONS } from './constants'; +import { ENV_TYPE_OPTIONS, FORM_FIELD_NAMES, IDE_CODER_OPTION, IDE_OPTIONS } from './constants'; import { IRunEnvironmentFormKeys, IRunEnvironmentFormValues } from './types'; @@ -57,6 +59,11 @@ const envValidationSchema = yup.object({ .matches(/^(https?):\/\/([^\s\/?#]+)((?:\/[^\s?#]*)*)(?::\/(.*))?$/i, urlFormatError) .required(requiredFieldError), }), + + password: yup.string().when('ide', { + is: 'coder', + then: yup.string().required(requiredFieldError), + }), }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -109,6 +116,14 @@ export const CreateDevEnvironment: React.FC = () => { const [getRunSpecFromYaml] = useGetRunSpecFromYaml({ projectName: selectedProject ?? '' }); + const { data: gatewaysData, isLoading: isLoadingGateways } = useGetProjectGatewaysQuery( + { projectName: selectedProject ?? '' }, + { skip: !selectedProject }, + ); + + const hasGateways = useMemo(() => { + return !isLoadingGateways && !!gatewaysData?.length; + }, [isLoadingGateways, gatewaysData]); const projectHavingFleetMap = useCheckingForFleetsInProjects({ projectNames: selectedProject ? [selectedProject] : [] }); const projectDontHasFleets = !!selectedProject && !projectHavingFleetMap[selectedProject]; @@ -143,6 +158,14 @@ export const CreateDevEnvironment: React.FC = () => { navigate(ROUTES.RUNS.LIST); }; + const ideOptions = useMemo(() => { + if (formValues.env_type === 'web') { + return [...IDE_OPTIONS, IDE_CODER_OPTION]; + } + + return IDE_OPTIONS; + }, [formValues.env_type]); + const validateOffer = async () => { return await trigger(['offer']); }; @@ -216,6 +239,14 @@ export const CreateDevEnvironment: React.FC = () => { setValue('offer', newSelectedOffers?.[0] ?? null); }; + const onChangeEventType: FormSelectProps['onChange'] = ({ detail }) => { + if (detail.selectedOption.value === 'web') { + setValue('ide', 'coder'); + } else { + setValue('ide', 'cursor'); + } + }; + const onSubmitWizard = async () => { const isValid = await trigger(); @@ -274,6 +305,16 @@ export const CreateDevEnvironment: React.FC = () => { setValue('config_yaml', yaml); }, [yaml]); + useEffect(() => { + if (!formValues.env_type && hasGateways) { + setValue('env_type', 'web'); + setValue('ide', 'coder'); + setValue('password', generateSecurePassword(20)); + } + }, [hasGateways, formValues.env_type]); + + const isDisabledIdeField = loading || formValues.env_type === 'web'; + return (
{ disabled={loading} /> + {hasGateways && ( + + )} + + + Password copied} + > +