diff --git a/data/schema.graphql b/data/schema.graphql index d0178a1e48..f91ced9a65 100644 --- a/data/schema.graphql +++ b/data/schema.graphql @@ -23,6 +23,85 @@ directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA +type AccessToken implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + + """Added in 25.13.0: The access token.""" + token: String! + + """Added in 25.13.0: The creation timestamp of the access token.""" + createdAt: DateTime! + + """Added in 25.13.0: The expiration timestamp of the access token.""" + validUntil: DateTime! +} + +"""Added in 25.13.0""" +type AccessTokenConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [AccessTokenEdge!]! + count: Int! +} + +"""An edge in a connection.""" +type AccessTokenEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: AccessToken! +} + +""" +Added in 25.13.0. This enum represents the activeness status of a replica, indicating whether the deployment is currently active and able to serve requests. +""" +enum ActivenessStatus + @join__type(graph: STRAWBERRY) +{ + ACTIVE @join__enumValue(graph: STRAWBERRY) + INACTIVE @join__enumValue(graph: STRAWBERRY) +} + +"""Added in 25.13.0""" +input ActivenessStatusFilter + @join__type(graph: STRAWBERRY) +{ + in: [ActivenessStatus!] = null + equals: ActivenessStatus = null +} + +"""Added in 25.13.0""" +input AddModelRevisionInput + @join__type(graph: STRAWBERRY) +{ + name: String = null + deploymentId: ID! + clusterConfig: ClusterConfigInput! + resourceConfig: ResourceConfigInput! + image: ImageInput! + modelRuntimeConfig: ModelRuntimeConfigInput! + modelMountConfig: ModelMountConfigInput! + extraMounts: [ExtraVFolderMountInput!] +} + +"""Added in 25.13.0""" +type AddModelRevisionPayload + @join__type(graph: STRAWBERRY) +{ + revision: ModelRevision! +} + type Agent implements Item @join__implements(graph: GRAPHENE, interface: "Item") @join__type(graph: GRAPHENE) @@ -570,9 +649,42 @@ enum AutoScalingMetricComparator """The source type to fetch metrics. Added in 25.1.0.""" enum AutoScalingMetricSource @join__type(graph: GRAPHENE) + @join__type(graph: STRAWBERRY) { - KERNEL @join__enumValue(graph: GRAPHENE) - INFERENCE_FRAMEWORK @join__enumValue(graph: GRAPHENE) + KERNEL @join__enumValue(graph: GRAPHENE) @join__enumValue(graph: STRAWBERRY) + INFERENCE_FRAMEWORK @join__enumValue(graph: GRAPHENE) @join__enumValue(graph: STRAWBERRY) +} + +type AutoScalingRule implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + + """Added in 25.13.0 (e.g. KERNEL, INFERENCE_FRAMEWORK)""" + metricSource: AutoScalingMetricSource! + metricName: String! + + """Added in 25.13.0: The minimum threshold for scaling (e.g. 0.5)""" + minThreshold: Decimal + + """Added in 25.13.0: The maximum threshold for scaling (e.g. 21.0)""" + maxThreshold: Decimal + + """Added in 25.13.0: The step size for scaling (e.g. 1).""" + stepSize: Int! + + """Added in 25.13.0: The time window (seconds) for scaling (e.g. 60).""" + timeWindow: Int! + + """Added in 25.13.0: The minimum number of replicas (e.g. 1).""" + minReplicas: Int + + """Added in 25.13.0: The maximum number of replicas (e.g. 10).""" + maxReplicas: Int + createdAt: DateTime! + lastTriggeredAt: DateTime! } """Added in 25.8.0.""" @@ -693,6 +805,7 @@ type ClearImages msg: String } +"""Added in 25.13.0""" type ClusterConfig @join__type(graph: STRAWBERRY) { @@ -700,6 +813,7 @@ type ClusterConfig size: Int! } +"""Added in 25.13.0""" input ClusterConfigInput @join__type(graph: STRAWBERRY) { @@ -707,6 +821,7 @@ input ClusterConfigInput size: Int! } +"""Added in 25.13.0""" enum ClusterMode @join__type(graph: STRAWBERRY) { @@ -864,75 +979,76 @@ type ComputeSessionList implements PaginatedList """Added in 24.09.0.""" type ComputeSessionNode implements Node @join__implements(graph: GRAPHENE, interface: "Node") - @join__type(graph: GRAPHENE) + @join__type(graph: GRAPHENE, key: "id") + @join__type(graph: STRAWBERRY, key: "id", extension: true) { """The ID of the object""" id: ID! """ID of session.""" - row_id: UUID - tag: String - name: String - type: String + row_id: UUID @join__field(graph: GRAPHENE) + tag: String @join__field(graph: GRAPHENE) + name: String @join__field(graph: GRAPHENE) + type: String @join__field(graph: GRAPHENE) """Added in 24.09.0.""" - priority: Int - cluster_template: String - cluster_mode: String - cluster_size: Int - domain_name: String - project_id: UUID - user_id: UUID + priority: Int @join__field(graph: GRAPHENE) + cluster_template: String @join__field(graph: GRAPHENE) + cluster_mode: String @join__field(graph: GRAPHENE) + cluster_size: Int @join__field(graph: GRAPHENE) + domain_name: String @join__field(graph: GRAPHENE) + project_id: UUID @join__field(graph: GRAPHENE) + user_id: UUID @join__field(graph: GRAPHENE) """Added in 25.13.0.""" - owner: UserNode - access_key: String + owner: UserNode @join__field(graph: GRAPHENE) + access_key: String @join__field(graph: GRAPHENE) """ One of ['read_attribute', 'update_attribute', 'delete_session', 'start_app', 'execute', 'convert_to_image']. """ - permissions: [SessionPermissionValueField] - status: String - status_info: String - status_data: JSONString - status_history: JSONString - created_at: DateTime - terminated_at: DateTime - starts_at: DateTime - scheduled_at: DateTime + permissions: [SessionPermissionValueField] @join__field(graph: GRAPHENE) + status: String @join__field(graph: GRAPHENE) + status_info: String @join__field(graph: GRAPHENE) + status_data: JSONString @join__field(graph: GRAPHENE) + status_history: JSONString @join__field(graph: GRAPHENE) + created_at: DateTime @join__field(graph: GRAPHENE) + terminated_at: DateTime @join__field(graph: GRAPHENE) + starts_at: DateTime @join__field(graph: GRAPHENE) + scheduled_at: DateTime @join__field(graph: GRAPHENE) """Added in 25.13.0.""" - queue_position: Int - startup_command: String - result: String - commit_status: String - abusing_reports: [JSONString] - idle_checks: JSONString - agent_ids: [String] - resource_opts: JSONString - scaling_group: String - service_ports: JSONString - vfolder_mounts: [String] - occupied_slots: JSONString - requested_slots: JSONString + queue_position: Int @join__field(graph: GRAPHENE) + startup_command: String @join__field(graph: GRAPHENE) + result: String @join__field(graph: GRAPHENE) + commit_status: String @join__field(graph: GRAPHENE) + abusing_reports: [JSONString] @join__field(graph: GRAPHENE) + idle_checks: JSONString @join__field(graph: GRAPHENE) + agent_ids: [String] @join__field(graph: GRAPHENE) + resource_opts: JSONString @join__field(graph: GRAPHENE) + scaling_group: String @join__field(graph: GRAPHENE) + service_ports: JSONString @join__field(graph: GRAPHENE) + vfolder_mounts: [String] @join__field(graph: GRAPHENE) + occupied_slots: JSONString @join__field(graph: GRAPHENE) + requested_slots: JSONString @join__field(graph: GRAPHENE) """Added in 25.4.0.""" - image_references: [String] + image_references: [String] @join__field(graph: GRAPHENE) """Added in 25.4.0.""" - vfolder_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): VirtualFolderConnection - num_queries: BigInt - inference_metrics: JSONString - kernel_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): KernelConnection + vfolder_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): VirtualFolderConnection @join__field(graph: GRAPHENE) + num_queries: BigInt @join__field(graph: GRAPHENE) + inference_metrics: JSONString @join__field(graph: GRAPHENE) + kernel_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): KernelConnection @join__field(graph: GRAPHENE) """Added in 24.09.0.""" - dependents(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection + dependents(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection @join__field(graph: GRAPHENE) """Added in 24.09.0.""" - dependees(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection + dependees(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection @join__field(graph: GRAPHENE) """Added in 24.09.0.""" - graph(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection + graph(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ComputeSessionConnection @join__field(graph: GRAPHENE) } """Deprecated since 24.09.0. use `ContainerRegistryNode` instead""" @@ -1063,6 +1179,44 @@ type ContainerUtilizationMetricMetadata metric_names: [String] } +input CreateAccessTokenInput + @join__type(graph: STRAWBERRY) +{ + """ + Added in 25.13.0: The ID of the model deployment for which the access token is created. + """ + modelDeploymentId: ID! + + """Added in 25.13.0: The expiration timestamp of the access token.""" + validUntil: DateTime! +} + +type CreateAccessTokenPayload + @join__type(graph: STRAWBERRY) +{ + accessToken: AccessToken! +} + +input CreateAutoScalingRuleInput + @join__type(graph: STRAWBERRY) +{ + modelDeploymentId: ID! + metricSource: AutoScalingMetricSource! + metricName: String! + minThreshold: Decimal + maxThreshold: Decimal + stepSize: Int! + timeWindow: Int! + minReplicas: Int + maxReplicas: Int +} + +type CreateAutoScalingRulePayload + @join__type(graph: STRAWBERRY) +{ + autoScalingRule: AutoScalingRule! +} + """Deprecated since 24.09.0. use `CreateContainerRegistryNode` instead""" type CreateContainerRegistry @join__type(graph: GRAPHENE) @@ -1245,33 +1399,38 @@ input CreateKeyPairResourcePolicyInput max_pending_session_resource_slots: JSONString } +"""Added in 25.13.0""" input CreateModelDeploymentInput @join__type(graph: STRAWBERRY) { metadata: ModelDeploymentMetadataInput! networkAccess: ModelDeploymentNetworkAccessInput! - clusterConfig: ClusterConfigInput! - resourceConfig: ResourceConfigInput! - deploymentStrategy: DeploymentStrategyInput! + defaultDeploymentStrategy: DeploymentStrategyInput! + desiredReplicaCount: Int! initialRevision: CreateModelRevisionInput! } +"""Added in 25.13.0""" type CreateModelDeploymentPayload @join__type(graph: STRAWBERRY) { deployment: ModelDeployment! } +"""Added in 25.13.0""" input CreateModelRevisionInput @join__type(graph: STRAWBERRY) { - deploymentId: ID! - name: String! + name: String = null + clusterConfig: ClusterConfigInput! + resourceConfig: ResourceConfigInput! image: ImageInput! modelRuntimeConfig: ModelRuntimeConfigInput! modelMountConfig: ModelMountConfigInput! + extraMounts: [ExtraVFolderMountInput!] } +"""Added in 25.13.0""" type CreateModelRevisionPayload @join__type(graph: STRAWBERRY) { @@ -1452,6 +1611,10 @@ type DealiasImage msg: String } +"""Decimal (fixed-point)""" +scalar Decimal + @join__type(graph: STRAWBERRY) + """Added in 25.15.0""" input DeleteArtifactsInput @join__type(graph: STRAWBERRY) @@ -1466,6 +1629,18 @@ type DeleteArtifactsPayload artifacts: [Artifact!]! } +input DeleteAutoScalingRuleInput + @join__type(graph: STRAWBERRY) +{ + id: ID! +} + +type DeleteAutoScalingRulePayload + @join__type(graph: STRAWBERRY) +{ + id: ID! +} + """Deprecated since 24.09.0. use `DeleteContainerRegistryNode` instead""" type DeleteContainerRegistry @join__type(graph: GRAPHENE) @@ -1547,16 +1722,18 @@ type DeleteKeyPairResourcePolicy msg: String } +"""Added in 25.13.0""" input DeleteModelDeploymentInput @join__type(graph: STRAWBERRY) { id: ID! } +"""Added in 25.13.0""" type DeleteModelDeploymentPayload @join__type(graph: STRAWBERRY) { - deployment: ModelDeployment + id: ID! } """Added in 24.12.0.""" @@ -1635,18 +1812,22 @@ type DeleteUserResourcePolicy msg: String } +"""Added in 25.13.0""" input DeploymentFilter @join__type(graph: STRAWBERRY) { - status: DeploymentStatus = null + name: StringFilter = null + status: DeploymentStatusFilter = null openToPublic: Boolean = null - tags: [StringFilter!] = null - AND: DeploymentFilter = null - OR: DeploymentFilter = null - NOT: DeploymentFilter = null - DISTINCT: Boolean = null + tags: StringFilter = null + endpointUrl: StringFilter = null + id: ID = null + AND: [DeploymentFilter!] = null + OR: [DeploymentFilter!] = null + NOT: [DeploymentFilter!] = null } +"""Added in 25.13.0""" input DeploymentOrderBy @join__type(graph: STRAWBERRY) { @@ -1654,6 +1835,7 @@ input DeploymentOrderBy direction: OrderDirection! = DESC } +"""Added in 25.13.0""" enum DeploymentOrderField @join__type(graph: STRAWBERRY) { @@ -1662,37 +1844,57 @@ enum DeploymentOrderField NAME @join__enumValue(graph: STRAWBERRY) } +""" +Added in 25.13.0. This enum represents the deployment status of a model deployment, indicating its current state. +""" enum DeploymentStatus @join__type(graph: STRAWBERRY) { - ACTIVE @join__enumValue(graph: STRAWBERRY) - INACTIVE @join__enumValue(graph: STRAWBERRY) + PENDING @join__enumValue(graph: STRAWBERRY) + SCALING @join__enumValue(graph: STRAWBERRY) + DEPLOYING @join__enumValue(graph: STRAWBERRY) + READY @join__enumValue(graph: STRAWBERRY) + STOPPING @join__enumValue(graph: STRAWBERRY) + STOPPED @join__enumValue(graph: STRAWBERRY) } +"""Added in 25.13.0""" type DeploymentStatusChangedPayload @join__type(graph: STRAWBERRY) { deployment: ModelDeployment! } +"""Added in 25.13.0""" +input DeploymentStatusFilter + @join__type(graph: STRAWBERRY) +{ + in: [DeploymentStatus!] = null + equals: DeploymentStatus = null +} + +"""Added in 25.13.0""" type DeploymentStrategy @join__type(graph: STRAWBERRY) { type: DeploymentStrategyType! } +"""Added in 25.13.0""" input DeploymentStrategyInput @join__type(graph: STRAWBERRY) { type: DeploymentStrategyType! } +""" +Added in 25.13.0. This enum represents the deployment strategy type of a model deployment, indicating the strategy used for deployment. +""" enum DeploymentStrategyType @join__type(graph: STRAWBERRY) { ROLLING @join__enumValue(graph: STRAWBERRY) BLUE_GREEN @join__enumValue(graph: STRAWBERRY) - CANARY @join__enumValue(graph: STRAWBERRY) } type DisassociateAllScalingGroupsWithDomain @@ -1808,21 +2010,22 @@ input DomainInput """Added in 24.12.0.""" type DomainNode implements Node @join__implements(graph: GRAPHENE, interface: "Node") - @join__type(graph: GRAPHENE) + @join__type(graph: GRAPHENE, key: "id") + @join__type(graph: STRAWBERRY, key: "id", extension: true) { """The ID of the object""" id: ID! - name: String - description: String - is_active: Boolean - created_at: DateTime - modified_at: DateTime - total_resource_slots: JSONString - allowed_vfolder_hosts: JSONString - allowed_docker_registries: [String] - dotfiles: Bytes - integration_id: String - scaling_groups(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ScalinGroupConnection + name: String @join__field(graph: GRAPHENE) + description: String @join__field(graph: GRAPHENE) + is_active: Boolean @join__field(graph: GRAPHENE) + created_at: DateTime @join__field(graph: GRAPHENE) + modified_at: DateTime @join__field(graph: GRAPHENE) + total_resource_slots: JSONString @join__field(graph: GRAPHENE) + allowed_vfolder_hosts: JSONString @join__field(graph: GRAPHENE) + allowed_docker_registries: [String] @join__field(graph: GRAPHENE) + dotfiles: Bytes @join__field(graph: GRAPHENE) + integration_id: String @join__field(graph: GRAPHENE) + scaling_groups(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ScalinGroupConnection @join__field(graph: GRAPHENE) } """ @@ -1945,22 +2148,21 @@ input EndpointAutoScalingRuleInput type EndpointAutoScalingRuleNode implements Node @join__implements(graph: GRAPHENE, interface: "Node") @join__type(graph: GRAPHENE, key: "id") - @join__type(graph: STRAWBERRY, key: "id", extension: true) { """The ID of the object""" id: ID! - row_id: UUID! @join__field(graph: GRAPHENE) - metric_source: AutoScalingMetricSource! @join__field(graph: GRAPHENE) - metric_name: String! @join__field(graph: GRAPHENE) - threshold: String! @join__field(graph: GRAPHENE) - comparator: AutoScalingMetricComparator! @join__field(graph: GRAPHENE) - step_size: Int! @join__field(graph: GRAPHENE) - cooldown_seconds: Int! @join__field(graph: GRAPHENE) - min_replicas: Int @join__field(graph: GRAPHENE) - max_replicas: Int @join__field(graph: GRAPHENE) - created_at: DateTime! @join__field(graph: GRAPHENE) - last_triggered_at: DateTime @join__field(graph: GRAPHENE) - endpoint: UUID! @join__field(graph: GRAPHENE) + row_id: UUID! + metric_source: AutoScalingMetricSource! + metric_name: String! + threshold: String! + comparator: AutoScalingMetricComparator! + step_size: Int! + cooldown_seconds: Int! + min_replicas: Int + max_replicas: Int + created_at: DateTime! + last_triggered_at: DateTime + endpoint: UUID! } type EndpointList implements PaginatedList @@ -1974,16 +2176,15 @@ type EndpointList implements PaginatedList type EndpointToken implements Item @join__implements(graph: GRAPHENE, interface: "Item") @join__type(graph: GRAPHENE, key: "token") - @join__type(graph: STRAWBERRY, key: "token", extension: true) { - id: ID @join__field(graph: GRAPHENE) + id: ID token: String! - endpoint_id: UUID! @join__field(graph: GRAPHENE) - domain: String! @join__field(graph: GRAPHENE) - project: String! @join__field(graph: GRAPHENE) - session_owner: UUID! @join__field(graph: GRAPHENE) - created_at: DateTime! @join__field(graph: GRAPHENE) - valid_until: DateTime @join__field(graph: GRAPHENE) + endpoint_id: UUID! + domain: String! + project: String! + session_owner: UUID! + created_at: DateTime! + valid_until: DateTime } type EndpointTokenList implements PaginatedList @@ -2012,6 +2213,47 @@ input ExtraMountInput permission: String } +type ExtraVFolderMount implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + mountDestination: String! + vfolder: VirtualFolderNode! +} + +"""Added in 25.13.0""" +type ExtraVFolderMountConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [ExtraVFolderMountEdge!]! + count: Int! +} + +"""An edge in a connection.""" +type ExtraVFolderMountEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: ExtraVFolderMount! +} + +"""Added in 25.13.0""" +input ExtraVFolderMountInput + @join__type(graph: STRAWBERRY) +{ + vfolderId: ID! + mountDestination: String +} + """ A string-serialized scalar represents a set of fields that's passed to a federated directive, such as @key, @requires, or @provides """ @@ -2145,34 +2387,35 @@ input GroupInput type GroupNode implements Node @join__implements(graph: GRAPHENE, interface: "Node") - @join__type(graph: GRAPHENE) + @join__type(graph: GRAPHENE, key: "id") + @join__type(graph: STRAWBERRY, key: "id", extension: true) { """The ID of the object""" id: ID! """Added in 24.03.7. The undecoded id value stored in DB.""" - row_id: UUID - name: String - description: String - is_active: Boolean - created_at: DateTime - modified_at: DateTime - domain_name: String - total_resource_slots: JSONString - allowed_vfolder_hosts: JSONString - integration_id: String - resource_policy: String + row_id: UUID @join__field(graph: GRAPHENE) + name: String @join__field(graph: GRAPHENE) + description: String @join__field(graph: GRAPHENE) + is_active: Boolean @join__field(graph: GRAPHENE) + created_at: DateTime @join__field(graph: GRAPHENE) + modified_at: DateTime @join__field(graph: GRAPHENE) + domain_name: String @join__field(graph: GRAPHENE) + total_resource_slots: JSONString @join__field(graph: GRAPHENE) + allowed_vfolder_hosts: JSONString @join__field(graph: GRAPHENE) + integration_id: String @join__field(graph: GRAPHENE) + resource_policy: String @join__field(graph: GRAPHENE) """Added in 24.03.7. One of ['GENERAL', 'MODEL_STORE'].""" - type: String + type: String @join__field(graph: GRAPHENE) """Added in 24.03.7.""" - container_registry: JSONString - scaling_groups: [String] + container_registry: JSONString @join__field(graph: GRAPHENE) + scaling_groups: [String] @join__field(graph: GRAPHENE) """Added in 25.3.0.""" - registry_quota: BigInt - user_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): UserConnection + registry_quota: BigInt @join__field(graph: GRAPHENE) + user_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): UserConnection @join__field(graph: GRAPHENE) } """ @@ -2280,6 +2523,7 @@ type ImageEdge cursor: String! } +"""Added in 25.13.0""" input ImageInput @join__type(graph: STRAWBERRY) { @@ -2429,6 +2673,13 @@ enum join__Graph { STRAWBERRY @join__graph(name: "strawberry", url: "http://host.docker.internal:8091/admin/gql/strawberry") } +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). +""" +scalar JSON + @join__type(graph: STRAWBERRY) + @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") + """ Allows use of a JSON String for input / output from the GraphQL schema. @@ -2671,6 +2922,26 @@ enum link__Purpose { EXECUTION } +""" +Added in 25.13.0. This enum represents the liveness status of a replica, indicating whether the deployment is currently running and able to serve requests. +""" +enum LivenessStatus + @join__type(graph: STRAWBERRY) +{ + NOT_CHECKED @join__enumValue(graph: STRAWBERRY) + HEALTHY @join__enumValue(graph: STRAWBERRY) + UNHEALTHY @join__enumValue(graph: STRAWBERRY) + DEGRADED @join__enumValue(graph: STRAWBERRY) +} + +"""Added in 25.13.0""" +input LivenessStatusFilter + @join__type(graph: STRAWBERRY) +{ + in: [LivenessStatus!] = null + equals: LivenessStatus = null +} + """Added in 25.6.0. A pair of timestamp and value.""" type MetricResultValue @join__type(graph: GRAPHENE) @@ -2748,6 +3019,7 @@ type ModelCardEdge cursor: String! } +"""Added in 25.13.0""" type ModelDeployment implements Node @join__implements(graph: STRAWBERRY, interface: "Node") @join__type(graph: STRAWBERRY) @@ -2757,15 +3029,14 @@ type ModelDeployment implements Node metadata: ModelDeploymentMetadata! networkAccess: ModelDeploymentNetworkAccess! revision: ModelRevision - revisionHistory: ModelRevisionConnection! scalingRule: ScalingRule! replicaState: ReplicaState! - deploymentStrategy: DeploymentStrategy! - clusterConfig: ClusterConfig! - resourceConfig: ResourceConfig! + defaultDeploymentStrategy: DeploymentStrategy! createdUser: UserNode! + revisionHistory(filter: ModelRevisionFilter = null, orderBy: [ModelRevisionOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ModelRevisionConnection! } +"""Added in 25.13.0""" type ModelDeploymentConnection @join__type(graph: STRAWBERRY) { @@ -2788,32 +3059,40 @@ type ModelDeploymentEdge node: ModelDeployment! } +"""Added in 25.13.0""" type ModelDeploymentMetadata @join__type(graph: STRAWBERRY) { name: String! status: DeploymentStatus! tags: [String!]! + project: GroupNode! + domain: DomainNode! createdAt: DateTime! updatedAt: DateTime! } +"""Added in 25.13.0""" input ModelDeploymentMetadataInput @join__type(graph: STRAWBERRY) { - name: String! + projectId: ID! + domainName: String! + name: String = null tags: [String!] = null } +"""Added in 25.13.0""" type ModelDeploymentNetworkAccess @join__type(graph: STRAWBERRY) { endpointUrl: String preferredDomainName: String openToPublic: Boolean! - accessTokens: [EndpointToken!]! + accessTokens: AccessTokenConnection! } +"""Added in 25.13.0""" input ModelDeploymentNetworkAccessInput @join__type(graph: STRAWBERRY) { @@ -2821,6 +3100,7 @@ input ModelDeploymentNetworkAccessInput openToPublic: Boolean! = false } +"""Added in 25.13.0""" type ModelMountConfig @join__type(graph: STRAWBERRY) { @@ -2829,6 +3109,7 @@ type ModelMountConfig definitionPath: String! } +"""Added in 25.13.0""" input ModelMountConfigInput @join__type(graph: STRAWBERRY) { @@ -2837,18 +3118,49 @@ input ModelMountConfigInput definitionPath: String! } +"""Added in 25.13.0""" type ModelReplica implements Node @join__implements(graph: STRAWBERRY, interface: "Node") @join__type(graph: STRAWBERRY) { """The Globally Unique ID of this object""" id: ID! - name: String! - status: ReplicaStatus! revision: ModelRevision! - routings: [RoutingNode!]! + + """ + This represents whether the replica has been checked and its health state. + """ + readinessStatus: ReadinessStatus! + + """ + This represents whether the replica is currently running and able to serve requests. + """ + livenessStatus: LivenessStatus! + + """ + This represents whether the replica is currently active and able to serve requests. + """ + activenessStatus: ActivenessStatus! + weight: Int! + + """ + Detailed information about the routing node. It can include both error messages and success messages. + """ + detail: JSONString! + createdAt: DateTime! + + """ + live statistics of the routing node. e.g. "live_stat": "{\"cpu_util\": {\"current\": \"7.472\", \"capacity\": \"1000\", \"pct\": \"0.75\", \"unit_hint\": \"percent\"}}" + """ + liveStat: JSONString! + + """ + The session ID associated with the replica. This can be null right after replica creation. + """ + session: ComputeSessionNode! } +"""Added in 25.13.0""" type ModelReplicaConnection @join__type(graph: STRAWBERRY) { @@ -2871,6 +3183,7 @@ type ModelReplicaEdge node: ModelReplica! } +"""Added in 25.13.0""" type ModelRevision implements Node @join__implements(graph: STRAWBERRY, interface: "Node") @join__type(graph: STRAWBERRY) @@ -2878,12 +3191,16 @@ type ModelRevision implements Node """The Globally Unique ID of this object""" id: ID! name: String! + clusterConfig: ClusterConfig! + resourceConfig: ResourceConfig! modelRuntimeConfig: ModelRuntimeConfig! modelMountConfig: ModelMountConfig! + extraMounts: ExtraVFolderMountConnection! image: ImageNode! createdAt: DateTime! } +"""Added in 25.13.0""" type ModelRevisionConnection @join__type(graph: STRAWBERRY) { @@ -2906,44 +3223,58 @@ type ModelRevisionEdge node: ModelRevision! } +"""Added in 25.13.0""" input ModelRevisionFilter @join__type(graph: STRAWBERRY) { name: StringFilter = null deploymentId: ID = null - AND: ModelRevisionFilter = null - OR: ModelRevisionFilter = null - NOT: ModelRevisionFilter = null - DISTINCT: Boolean = null + id: ID = null + AND: [ModelRevisionFilter!] = null + OR: [ModelRevisionFilter!] = null + NOT: [ModelRevisionFilter!] = null } -input ModelRevisionOrder +"""Added in 25.13.0""" +input ModelRevisionOrderBy @join__type(graph: STRAWBERRY) { field: ModelRevisionOrderField! direction: OrderDirection! = DESC } +"""Added in 25.13.0""" enum ModelRevisionOrderField @join__type(graph: STRAWBERRY) { CREATED_AT @join__enumValue(graph: STRAWBERRY) NAME @join__enumValue(graph: STRAWBERRY) + ID @join__enumValue(graph: STRAWBERRY) } +"""Added in 25.13.0""" type ModelRuntimeConfig @join__type(graph: STRAWBERRY) { runtimeVariant: String! - serviceConfig: ServiceConfig + inferenceRuntimeConfig: JSON + + """ + Environment variables for the service, e.g. {"CUDA_VISIBLE_DEVICES": "0"} + """ environ: JSONString } +"""Added in 25.13.0""" input ModelRuntimeConfigInput @join__type(graph: STRAWBERRY) { runtimeVariant: String! - serviceConfig: JSONString = null + inferenceRuntimeConfig: JSON = null + + """ + Environment variables for the service, e.g. {"CUDA_VISIBLE_DEVICES": "0"} + """ environ: JSONString = null } @@ -3780,9 +4111,25 @@ type Mutation """Added in 25.14.0""" cancelImportArtifact(input: CancelArtifactInput!): CancelImportArtifactPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" createModelDeployment(input: CreateModelDeploymentInput!): CreateModelDeploymentPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" updateModelDeployment(input: UpdateModelDeploymentInput!): UpdateModelDeploymentPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" deleteModelDeployment(input: DeleteModelDeploymentInput!): DeleteModelDeploymentPayload! @join__field(graph: STRAWBERRY) + + """ + Added in 25.13.0. Force syncs up-to-date replica information. In normal situations this will be automatically handled by Backend.AI schedulers + """ + syncReplicas(input: SyncReplicaInput!): SyncReplicaPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" + addModelRevision(input: AddModelRevisionInput!): AddModelRevisionPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" createModelRevision(input: CreateModelRevisionInput!): CreateModelRevisionPayload! @join__field(graph: STRAWBERRY) """Added in 25.14.0""" @@ -3791,22 +4138,23 @@ type Mutation """Added in 25.14.0""" updateObjectStorage(input: UpdateObjectStorageInput!): UpdateObjectStoragePayload! @join__field(graph: STRAWBERRY) + """Added in 25.13.0""" + createAutoScalingRule(input: CreateAutoScalingRuleInput!): CreateAutoScalingRulePayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" + updateAutoScalingRule(input: UpdateAutoScalingRuleInput!): UpdateAutoScalingRulePayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" + deleteAutoScalingRule(input: DeleteAutoScalingRuleInput!): DeleteAutoScalingRulePayload! @join__field(graph: STRAWBERRY) + """Added in 25.14.0""" deleteObjectStorage(input: DeleteObjectStorageInput!): DeleteObjectStoragePayload! @join__field(graph: STRAWBERRY) - """ - Added in 25.15.0. - - Registers a new namespace within a storage. - """ - registerStorageNamespace(input: RegisterStorageNamespaceInput!): RegisterStorageNamespacePayload! @join__field(graph: STRAWBERRY) + """Added in 25.14.0""" + registerObjectStorageBucket(input: RegisterObjectStorageBucketInput!): RegisterObjectStorageBucketPayload! @join__field(graph: STRAWBERRY) - """ - Added in 25.15.0. - - Unregisters an existing namespace from a storage. - """ - unregisterStorageNamespace(input: UnregisterStorageNamespaceInput!): UnregisterStorageNamespacePayload! @join__field(graph: STRAWBERRY) + """Added in 25.14.0""" + unregisterObjectStorageBucket(input: UnregisterObjectStorageBucketInput!): UnregisterObjectStorageBucketPayload! @join__field(graph: STRAWBERRY) """Added in 25.14.0""" createHuggingfaceRegistry(input: CreateHuggingFaceRegistryInput!): CreateHuggingFaceRegistryPayload! @join__field(graph: STRAWBERRY) @@ -3837,6 +4185,9 @@ type Mutation """Added in 25.14.0""" rejectArtifactRevision(input: RejectArtifactInput!): RejectArtifactPayload! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0""" + createAccessToken(input: CreateAccessTokenInput!): CreateAccessTokenPayload! @join__field(graph: STRAWBERRY) } """Added in 24.12.0.""" @@ -3907,7 +4258,7 @@ type ObjectStorage implements Node secretKey: String! endpoint: String! region: String! - namespaces(before: String, after: String, first: Int, last: Int, limit: Int, offset: Int): StorageNamespaceConnection! + namespaces(before: String, after: String, first: Int, last: Int, limit: Int, offset: Int): ObjectStorageNamespaceConnection! } """Added in 25.14.0""" @@ -3933,6 +4284,40 @@ type ObjectStorageEdge node: ObjectStorage! } +"""Added in 25.14.0""" +type ObjectStorageNamespace implements Node + @join__implements(graph: STRAWBERRY, interface: "Node") + @join__type(graph: STRAWBERRY) +{ + """The Globally Unique ID of this object""" + id: ID! + storageId: ID! + bucket: String! +} + +"""Added in 25.14.0""" +type ObjectStorageNamespaceConnection + @join__type(graph: STRAWBERRY) +{ + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [ObjectStorageNamespaceEdge!]! + count: Int! +} + +"""An edge in a connection.""" +type ObjectStorageNamespaceEdge + @join__type(graph: STRAWBERRY) +{ + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: ObjectStorageNamespace! +} + enum OrderDirection @join__type(graph: STRAWBERRY) { @@ -4490,40 +4875,23 @@ type Query """Added in 25.14.0""" artifactRevisions(filter: ArtifactRevisionFilter = null, orderBy: [ArtifactRevisionOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ArtifactRevisionConnection! @join__field(graph: STRAWBERRY) - deployments( - filter: DeploymentFilter = null - orderBy: DeploymentOrderBy = null - - """Returns the first n items from the list.""" - first: Int = null - - """Returns the items in the list that come after the specified cursor.""" - after: String = null - """Returns the items in the list that come before the specified cursor.""" - before: String = null + """Added in 25.13.0""" + deployments(filter: DeploymentFilter = null, orderBy: [DeploymentOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ModelDeploymentConnection! @join__field(graph: STRAWBERRY) - """Returns the items in the list that come after the specified cursor.""" - last: Int = null - ): ModelDeploymentConnection! @join__field(graph: STRAWBERRY) + """Added in 25.13.0""" deployment(id: ID!): ModelDeployment @join__field(graph: STRAWBERRY) - revisions( - filter: ModelRevisionFilter = null - order: ModelRevisionOrder = null - """Returns the first n items from the list.""" - first: Int = null + """Added in 25.13.0""" + revisions(filter: ModelRevisionFilter = null, orderBy: [ModelRevisionOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ModelRevisionConnection! @join__field(graph: STRAWBERRY) - """Returns the items in the list that come after the specified cursor.""" - after: String = null + """Added in 25.13.0""" + revision(id: ID!): ModelRevision! @join__field(graph: STRAWBERRY) - """Returns the items in the list that come before the specified cursor.""" - before: String = null + """Added in 25.13.0""" + replicas(filter: ReplicaFilter = null, orderBy: [ReplicaOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ModelReplicaConnection! @join__field(graph: STRAWBERRY) - """Returns the items in the list that come after the specified cursor.""" - last: Int = null - ): ModelRevisionConnection! @join__field(graph: STRAWBERRY) - revision(id: ID!): ModelRevision @join__field(graph: STRAWBERRY) + """Added in 25.13.0""" replica(id: ID!): ModelReplica @join__field(graph: STRAWBERRY) """Added in 25.14.0""" @@ -4546,6 +4914,14 @@ type Query """Added in 25.14.0""" defaultArtifactRegistry(artifactType: ArtifactType!): ArtifactRegistry @join__field(graph: STRAWBERRY) + + """ + Added in 25.13.0 Get configuration JSON Schemas for all inference runtimes + """ + inferenceRuntimeConfigs: JSON! @join__field(graph: STRAWBERRY) + + """Added in 25.13.0. Get JSON Schema for inference runtime configuration""" + inferenceRuntimeConfig(name: String!): JSON! @join__field(graph: STRAWBERRY) } type QuotaDetails @@ -4572,31 +4948,35 @@ input QuotaScopeInput hard_limit_bytes: BigInt } -type RawServiceConfig +""" +Added in 25.13.0. This enum represents the readiness status of a replica, indicating whether the deployment has been checked and its health state. +""" +enum ReadinessStatus @join__type(graph: STRAWBERRY) { - config: JSONString! - extraCliParameters: String + NOT_CHECKED @join__enumValue(graph: STRAWBERRY) + HEALTHY @join__enumValue(graph: STRAWBERRY) + UNHEALTHY @join__enumValue(graph: STRAWBERRY) } -""" -Added in 25.15.0. +"""Added in 25.13.0""" +input ReadinessStatusFilter + @join__type(graph: STRAWBERRY) +{ + in: [ReadinessStatus!] = null + equals: ReadinessStatus = null +} -Input type for registering a storage namespace. -""" -input RegisterStorageNamespaceInput +"""Added in 25.14.0""" +input RegisterObjectStorageBucketInput @join__type(graph: STRAWBERRY) { storageId: UUID! - namespace: String! + bucketName: String! } -""" -Added in 25.15.0. - -Payload returned after storage namespace registration. -""" -type RegisterStorageNamespacePayload +"""Added in 25.14.0""" +type RegisterObjectStorageBucketPayload @join__type(graph: STRAWBERRY) { id: UUID! @@ -4616,20 +4996,43 @@ type RejectArtifactPayload artifactRevision: ArtifactRevision! } -type ReplicaState +"""Added in 25.13.0""" +input ReplicaFilter @join__type(graph: STRAWBERRY) { - desiredReplicaCount: Int! - replicas: ModelReplicaConnection! + readinessStatus: ReadinessStatusFilter = null + livenessStatus: LivenessStatusFilter = null + activenessStatus: ActivenessStatusFilter = null + id: ID = null + AND: [ReplicaFilter!] = null + OR: [ReplicaFilter!] = null + NOT: [ReplicaFilter!] = null } -enum ReplicaStatus +"""Added in 25.13.0""" +input ReplicaOrderBy @join__type(graph: STRAWBERRY) { - HEALTHY @join__enumValue(graph: STRAWBERRY) - UNHEALTHY @join__enumValue(graph: STRAWBERRY) + field: ReplicaOrderField! + direction: OrderDirection! = DESC } +"""Added in 25.13.0""" +enum ReplicaOrderField + @join__type(graph: STRAWBERRY) +{ + CREATED_AT @join__enumValue(graph: STRAWBERRY) +} + +"""Added in 25.13.0""" +type ReplicaState + @join__type(graph: STRAWBERRY) +{ + desiredReplicaCount: Int! + replicas(filter: ReplicaFilter = null, orderBy: [ReplicaOrderBy!] = null, before: String = null, after: String = null, first: Int = null, last: Int = null, limit: Int = null, offset: Int = null): ModelReplicaConnection! +} + +"""Added in 25.13.0""" type ReplicaStatusChangedPayload @join__type(graph: STRAWBERRY) { @@ -4688,22 +5091,41 @@ type ReservoirRegistryEdge node: ReservoirRegistry! } +"""Added in 25.13.0""" type ResourceConfig @join__type(graph: STRAWBERRY) { resourceGroup: ScalingGroupNode! + + """ + Resource Slots are a JSON string that describes the resources allocated for the deployment. Example: "resourceSlots": "{\"cpu\": \"1\", \"mem\": \"1073741824\", \"cuda.device\": \"0\"}" + """ resourceSlots: JSONString! + + """ + Resource Options are a JSON string that describes additional options for the resources. This is especially used for shared memory configurations. Example: "resourceOpts": "{\"shmem\": \"64m\"}" + """ resourceOpts: JSONString } +"""Added in 25.13.0""" input ResourceConfigInput @join__type(graph: STRAWBERRY) { resourceGroup: ResourceGroupInput! + + """ + Resources allocated for the deployment. Example: "resourceSlots": "{\"cpu\": \"1\", \"mem\": \"1073741824\", \"cuda.device\": \"0\"}" + """ resourceSlots: JSONString! + + """ + Additional options for the resources. This is especially used for shared memory configurations. Example: "resourceOpts": "{\"shmem\": \"64m\"}" + """ resourceOpts: JSONString = null } +"""Added in 25.13.0""" input ResourceGroupInput @join__type(graph: STRAWBERRY) { @@ -4780,21 +5202,6 @@ type RoutingList implements PaginatedList total_count: Int! } -type RoutingNode implements Node - @join__implements(graph: STRAWBERRY, interface: "Node") - @join__type(graph: STRAWBERRY) -{ - """The Globally Unique ID of this object""" - id: ID! - routingId: UUID! - endpoint: String! - session: UUID! - status: String! - trafficRatio: Float! - createdAt: DateTime! - liveStat: JSONString! -} - """Added in 24.03.5.""" type RuntimeVariantInfo @join__type(graph: GRAPHENE) @@ -4898,10 +5305,11 @@ type ScalinGroupEdge cursor: String! } +"""Added in 25.13.0""" type ScalingRule @join__type(graph: STRAWBERRY) { - autoScalingRules: [EndpointAutoScalingRuleNode!]! + autoScalingRules: [AutoScalingRule!]! } """Added in 25.14.0""" @@ -4944,12 +5352,6 @@ Added in 24.12.0. A string value in the format ':'. import('./components/FolderInvitationResponseModalOpener'), ); -const FileUploadManager = React.lazy( - () => import('./components/FileUploadManager'), -); const ServiceLauncherCreatePage = React.lazy( () => import('./components/ServiceLauncherPageContent'), ); @@ -90,6 +87,16 @@ const ReservoirArtifactDetailPage = React.lazy( ); const SchedulerPage = React.lazy(() => import('./pages/SchedulerPage')); +// Deployment pages +const DeploymentListPage = React.lazy( + () => import('./pages/DeploymentListPage'), +); +const DeploymentDetailPage = React.lazy( + () => import('./pages/DeploymentDetailPage'), +); +const DeploymentLauncherPage = React.lazy( + () => import('./pages/DeploymentLauncherPage'), +); interface CustomHandle { title?: string; @@ -104,36 +111,31 @@ const router = createBrowserRouter([ path: '/interactive-login', errorElement: , element: ( - - - - - + + + ), }, { path: '/', errorElement: , element: ( - - - - - - - - - - - + } + > + + + + + + + ), children: [ { @@ -312,6 +314,58 @@ const router = createBrowserRouter([ }, ], }, + { + path: '/deployment', + handle: { labelKey: 'webui.menu.Deployment' }, + children: [ + { + path: '', + Component: () => { + const { t } = useTranslation(); + useSuspendedBackendaiClient(); + return ( + + + } + > + + + + ); + }, + }, + { + path: '/deployment/start', + handle: { labelKey: 'deployment.launcher.CreateNewDeployment' }, + element: ( + + + + + } + > + + + + ), + }, + { + path: '/deployment/:deploymentId', + handle: { labelKey: 'deployment.DeploymentDetail' }, + element: ( + + }> + + + + ), + }, + ], + }, { path: '/service', handle: { labelKey: 'webui.menu.Serving' }, @@ -588,7 +642,11 @@ const router = createBrowserRouter([ ]); const App: FC = () => { - return ; + return ( + + + + ); }; export default App; diff --git a/react/src/components/DeploymentLauncherPreview.tsx b/react/src/components/DeploymentLauncherPreview.tsx new file mode 100644 index 0000000000..691512180c --- /dev/null +++ b/react/src/components/DeploymentLauncherPreview.tsx @@ -0,0 +1,724 @@ +import { preserveDotStartCase, getImageFullName } from '../helper'; +import { + useBackendAIImageMetaData, + useSuspendedBackendaiClient, +} from '../hooks'; +import { useThemeMode } from '../hooks/useThemeMode'; +import { + DeploymentLauncherFormValue, + DeploymentLauncherStepKey, +} from '../pages/DeploymentLauncherPage'; +import { ResourceNumbersOfSession } from '../pages/SessionLauncherPage'; +import DoubleTag from './DoubleTag'; +import ImageMetaIcon from './ImageMetaIcon'; +import { ImageTags } from './ImageTags'; +import VFolderLazyView from './VFolderLazyView'; +import { + Typography, + Form, + Descriptions, + Tag, + Row, + Col, + Divider, + Card, + Table, + theme, +} from 'antd'; +import { BAICard, BAIFlex } from 'backend.ai-ui'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { dark } from 'react-syntax-highlighter/dist/esm/styles/hljs'; + +const DeploymentLauncherPreview: React.FC<{ + onClickEditStep: (stepKey: DeploymentLauncherStepKey) => void; +}> = ({ onClickEditStep }) => { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + const { isDarkMode } = useThemeMode(); + const { token } = theme.useToken(); + const baiClient = useSuspendedBackendaiClient(); + const supportExtendedImageInfo = + baiClient?.supports('extended-image-info') ?? false; + const [, { getBaseVersion, getBaseImage, tagAlias }] = + useBackendAIImageMetaData(); + + return ( + + {/* Metadata Summary */} + fieldError.errors.length > 0, + ) + ? 'error' + : undefined + } + extraButtonTitle={t('button.Edit')} + onClickExtraButton={() => onClickEditStep('deployment')} + > + + {t('general.None')} + + ), + }, + ...(form.getFieldValue(['metadata', 'tags'])?.length > 0 + ? [ + { + key: 'tags', + label: t('deployment.launcher.Tags'), + children: ( + + {form + .getFieldValue(['metadata', 'tags']) + .map((tag: string, index: number) => ( + {tag} + ))} + + ), + }, + ] + : []), + { + key: 'defaultDeploymentStrategy', + label: t('deployment.launcher.defaultDeploymentStrategy'), + children: + form.getFieldValue(['defaultDeploymentStrategy', 'type']) === + 'ROLLING' ? ( + t('deployment.launcher.RollingUpdate') + ) : form.getFieldValue([ + 'defaultDeploymentStrategy', + 'type', + ]) === 'BLUE_GREEN' ? ( + t('deployment.launcher.BlueGreenDeployment') + ) : form.getFieldValue([ + 'defaultDeploymentStrategy', + 'type', + ]) === 'CANARY' ? ( + t('deployment.launcher.CanaryDeployment') + ) : ( + + {t('general.None')} + + ), + }, + { + key: 'desiredReplicas', + label: t('deployment.NumberOfDesiredReplicas'), + children: form.getFieldValue(['desiredReplicaCount']), + }, + ...(form.getFieldValue(['networkAccess', 'preferredDomainName']) + ? [ + { + key: 'preferredDomainName', + label: t('deployment.launcher.PreferredDomainName'), + children: ( + + {form.getFieldValue([ + 'networkAccess', + 'preferredDomainName', + ])} + + ), + }, + ] + : []), + { + key: 'openToPublic', + label: t('deployment.launcher.OpenToPublic'), + children: form.getFieldValue(['networkAccess', 'openToPublic']) + ? t('button.Yes') + : t('button.No'), + }, + ]} + /> + + + {/* Initial Revisions Summary */} + fieldError.errors.length > 0, + ) || + _.some( + form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'inferenceRuntimeConfig', + ]) as Array<{ variable: string; value: string }>, + (v, idx) => { + return ( + form.getFieldError([ + 'initialRevision', + 'modelRuntimeConfig', + 'inferenceRuntimeConfig', + // @ts-ignore + idx, + // @ts-ignore + 'variable', + ]).length > 0 || + form.getFieldError([ + 'initialRevision', + 'modelRuntimeConfig', + 'inferenceRuntimeConfig', + // @ts-ignore + idx, + // @ts-ignore + 'value', + ]).length > 0 + ); + }, + ) || + _.some( + form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'environ', + ]) as Array<{ variable: string; value: string }>, + (v, idx) => { + return ( + form.getFieldError([ + 'initialRevision', + 'modelRuntimeConfig', + 'environ', + // @ts-ignore + idx, + // @ts-ignore + 'variable', + ]).length > 0 || + form.getFieldError([ + 'initialRevision', + 'modelRuntimeConfig', + 'environ', + // @ts-ignore + idx, + // @ts-ignore + 'value', + ]).length > 0 + ); + }, + ) + ? 'error' + : undefined + } + extraButtonTitle={t('button.Edit')} + onClickExtraButton={() => onClickEditStep('revisions')} + > + {form.getFieldValue(['initialRevision', 'name'])?.length > 0 && ( + + {t('general.None')} + + ), + }, + ]} + /> + )} + + + + + + + {form.getFieldValue(['environments', 'manual']) ? ( + + {form.getFieldValue(['environments', 'manual'])} + + ) : ( + <> + + {tagAlias( + form.getFieldValue(['environments', 'image']) + ?.base_image_name, + )} + + + + { + form.getFieldValue(['environments', 'image']) + ?.version + } + + + + { + form.getFieldValue(['environments', 'image']) + ?.architecture + } + + + {/* TODO: replace this with AliasedImageDoubleTags after image list query with ImageNode is implemented. */} + {_.map( + form.getFieldValue(['environments', 'image'])?.tags, + (tag: { key: string; value: string }) => { + const isCustomized = _.includes( + tag.key, + 'customized_', + ); + const tagValue = isCustomized + ? _.find( + form.getFieldValue([ + 'environments', + 'image', + ])?.labels, + { + key: 'ai.backend.customized-image.name', + }, + )?.value + : tag.value; + const aliasedTag = tagAlias(tag.key + tagValue); + return _.isEqual( + aliasedTag, + preserveDotStartCase(tag.key + tagValue), + ) || isCustomized ? ( + + ) : ( + + {aliasedTag} + + ); + }, + )} + + + )} + + + + ) : ( + + + + + + + {form.getFieldValue(['environments', 'manual']) ? ( + + {form.getFieldValue(['environments', 'manual'])} + + ) : ( + <> + + {tagAlias( + getBaseImage( + form.getFieldValue(['environments', 'version']), + ), + )} + + + + {getBaseVersion( + form.getFieldValue(['environments', 'version']), + )} + + + + { + form.getFieldValue(['environments', 'image']) + ?.architecture + } + + + + } + /> + + + )} + + + + ), + }, + ]} + /> + + {/* FIXME: Antd bug: If Divider is inside Descriptions, the style may not be applied correctly */} + + {t('deployment.launcher.RuntimeAndMountConfig')} + + + + {t('general.None')} + + ), + }, + { + key: 'modelStorageToMount', + label: t('deployment.launcher.ModelStorageToMount'), + children: form.getFieldValue([ + 'initialRevision', + 'modelMountConfig', + 'vfolderId', + ]) ? ( + + ) : ( + + {t('general.None')} + + ), + }, + { + key: 'mountDestination', + label: t('deployment.launcher.MountDestination'), + children: form.getFieldValue([ + 'initialRevision', + 'modelMountConfig', + 'mountDestination', + ]) || /models, + }, + { + key: 'definitionPath', + label: t('deployment.launcher.DefinitionPath'), + children: form.getFieldValue([ + 'initialRevision', + 'modelMountConfig', + 'definitionPath', + ]) || model-definition.yaml, + }, + ...(form.getFieldValue('mount_ids')?.length > 0 + ? [ + { + key: 'additionalMounts', + label: t('deployment.launcher.AdditionalMounts'), + span: 2, + children: ( + { + return _.isEmpty(value) ? ( + + {`/home/work/${record.name}`} + + ) : ( + value + ); + }, + }, + ]} + dataSource={_.map( + form.getFieldValue('mount_ids'), + (v) => { + const name = + form.getFieldValue('vfoldersNameMap')?.[v] || v; + return { + name, + alias: form.getFieldValue('mount_id_map')?.[v], + }; + }, + )} + /> + ), + }, + ] + : []), + ...(form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'inferenceRuntimeConfig', + ])?.length > 0 + ? [ + { + key: 'inferenceRuntimeConfig', + label: t('deployment.launcher.InferenceRuntimeConfig'), + children: ( + + {_.map( + form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'inferenceRuntimeConfig', + ]) || [], + (v: { variable: string; value: string }) => + `${v?.variable || ''}="${v?.value || ''}"`, + ).join('\n')} + + ), + }, + ] + : []), + ...(form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'environ', + ])?.length > 0 + ? [ + { + key: 'environmentVariables', + label: t('deployment.launcher.EnvironmentVariables'), + children: ( + + {_.map( + form.getFieldValue([ + 'initialRevision', + 'modelRuntimeConfig', + 'environ', + ]) || [], + (v: { variable: string; value: string }) => + `${v?.variable || ''}="${v?.value || ''}"`, + ).join('\n')} + + ), + }, + ] + : []), + ]} + /> + + + {t('deployment.launcher.ResourceAllocation')} + + + + {t('general.None')} + + ), + }, + { + key: 'resourceAllocation', + label: t('deployment.launcher.ResourceAllocation'), + children: ( + + {form.getFieldValue('allocationPreset') === 'custom' ? ( + // Custom allocation - no preset tag + '' + ) : ( + {form.getFieldValue('allocationPreset')} + )} + + + + ), + }, + ]} + /> + + + + + + + + ); +}; + +export default DeploymentLauncherPreview; diff --git a/react/src/components/DeploymentMetadataFormItem.tsx b/react/src/components/DeploymentMetadataFormItem.tsx new file mode 100644 index 0000000000..b495f375f7 --- /dev/null +++ b/react/src/components/DeploymentMetadataFormItem.tsx @@ -0,0 +1,49 @@ +import { Form, Input, Select } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface DeploymentMetadataFormValues { + metadata: { + name: string; + tags?: string[]; + }; +} + +const DeploymentMetadataFormItem: React.FC = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + + + + + ); +}; + +export default DeploymentNetworkAccessFormItem; diff --git a/react/src/components/DeploymentRevisionRuntimeAndMountFormItem.tsx b/react/src/components/DeploymentRevisionRuntimeAndMountFormItem.tsx new file mode 100644 index 0000000000..37a0bb95e0 --- /dev/null +++ b/react/src/components/DeploymentRevisionRuntimeAndMountFormItem.tsx @@ -0,0 +1,179 @@ +import { useBaiSignedRequestWithPromise } from '../helper'; +import { useSuspenseTanQuery } from '../hooks/reactQueryAlias'; +import EnvVarFormList from './EnvVarFormList'; +import VFolderSelect from './VFolderSelect'; +import VFolderTableFormItem from './VFolderTableFormItem'; +import { MinusOutlined } from '@ant-design/icons'; +import { Form, Input, Select, theme } from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface DeploymentRevisionRuntimeAndMountFormItemProps { + initialVfolderId?: string; +} + +const DeploymentRevisionRuntimeAndMountFormItem: React.FC< + DeploymentRevisionRuntimeAndMountFormItemProps +> = ({ initialVfolderId }) => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const baiRequestWithPromise = useBaiSignedRequestWithPromise(); + + const { data: availableRuntimes } = useSuspenseTanQuery<{ + runtimes: { name: string; human_readable_name: string }[]; + }>({ + queryKey: ['baiClient.modelService.runtime.list'], + queryFn: () => { + return baiRequestWithPromise({ + method: 'GET', + url: `/services/_/runtimes`, + }); + }, + staleTime: 1000, + }); + + return ( + <> + + + + + + + + + ) + } + + + {({ getFieldValue }) => { + return ( + + vf.id !== + getFieldValue([ + 'initialRevision', + 'modelMountConfig', + 'vfolderId', + ]) && + vf.status === 'ready' && + vf.usage_mode !== 'model' && + !vf.name?.startsWith('.') + } + tableProps={{ + size: 'small', + }} + /> + ); + }} + + + {/* TODO: Add auto-complete and validation according to variable types. */} + + + + + + + ); +}; + +export default DeploymentRevisionRuntimeAndMountFormItem; diff --git a/react/src/components/DeploymentStrategyFormItem.tsx b/react/src/components/DeploymentStrategyFormItem.tsx new file mode 100644 index 0000000000..d87a751a78 --- /dev/null +++ b/react/src/components/DeploymentStrategyFormItem.tsx @@ -0,0 +1,131 @@ +import InputNumberWithSlider from './InputNumberWithSlider'; +import QuestionIconWithTooltip from './QuestionIconWithTooltip'; +import { Form, Radio, Typography, Alert, theme } from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +export interface DeploymentStrategyFormValues { + defaultDeploymentStrategy: { + type: 'ROLLING' | 'BLUE_GREEN' | 'CANARY'; + }; +} + +const DeploymentStrategyFormItem: React.FC = () => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + + const strategyDescriptions = { + ROLLING: t('deployment.launcher.RollingDescription'), + BLUE_GREEN: t('deployment.launcher.BlueGreenDescription'), + CANARY: t('deployment.launcher.CanaryDescription'), + }; + + return ( + <> + + + + + + + + {t('deployment.launcher.RollingUpdate')} + + + + + + + + + + {t('deployment.launcher.BlueGreenDeployment')} + + + + + + {/* TODO: Uncomment when CANARY strategy is implemented */} + {/* + + + {t('deployment.launcher.CanaryDeployment')} + + + + */} + + + + + {({ getFieldValue }) => { + const strategyType = getFieldValue([ + 'defaultDeploymentStrategy', + 'type', + ]); + + if (strategyType === 'BLUE_GREEN' || strategyType === 'CANARY') { + return ( + + } + type="warning" + showIcon + style={{ + marginBottom: token.marginLG, + }} + /> + ); + } + + return null; + }} + + + + + + ); +}; + +export default DeploymentStrategyFormItem; diff --git a/react/src/components/Deployments/DeploymentCreateModal.tsx b/react/src/components/Deployments/DeploymentCreateModal.tsx new file mode 100644 index 0000000000..eca597ca74 --- /dev/null +++ b/react/src/components/Deployments/DeploymentCreateModal.tsx @@ -0,0 +1,178 @@ +import { useWebUINavigate } from '../../hooks'; +import { Form, Input, Button, Modal } from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface DeploymentCreateFormValues { + name: string; + domain?: string; + description?: string; +} + +interface DeploymentCreateModalProps { + open: boolean; + onClose: () => void; + onSuccess?: () => void; +} + +const DeploymentCreateModal: React.FC = ({ + open, + onClose, + onSuccess, +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const webuiNavigate = useWebUINavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + // const [isCheckingDomain, setIsCheckingDomain] = useState(false); + const [domainCheckStatus, setDomainCheckStatus] = useState< + 'success' | 'error' | undefined + >(); + + const handleSubmit = async (values: DeploymentCreateFormValues) => { + setIsSubmitting(true); + try { + // Mock API call - replace with actual implementation + console.log('Creating deployment:', values); + + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Reset form and close modal + form.resetFields(); + onClose(); + + // Call success callback if provided + if (onSuccess) { + onSuccess(); + } + + // Navigate to deployment detail page after creation + webuiNavigate(`/deployment/mock-id`); + } catch (error) { + console.error('Failed to create deployment:', error); + } finally { + setIsSubmitting(false); + } + }; + + // const handleDomainCheck = async () => { + // const domain = form.getFieldValue('domain'); + // if (!domain) { + // return; + // } + + // setIsCheckingDomain(true); + // setDomainCheckStatus(undefined); + + // try { + // // Mock API call - replace with actual domain check implementation + // console.log('Checking domain:', domain); + + // // Simulate API delay + // await new Promise((resolve) => setTimeout(resolve, 1000)); + + // // Mock logic: domains starting with 'test' are considered duplicates + // const isDuplicate = domain.toLowerCase().startsWith('test'); + + // if (isDuplicate) { + // setDomainCheckStatus('error'); + // form.setFields([ + // { + // name: 'domain', + // errors: [t('deployment.DomainAlreadyExists')], + // }, + // ]); + // } else { + // setDomainCheckStatus('success'); + // form.setFields([ + // { + // name: 'domain', + // errors: [], + // }, + // ]); + // } + // } catch (error) { + // console.error('Failed to check domain:', error); + // setDomainCheckStatus('error'); + // } finally { + // setIsCheckingDomain(false); + // } + // }; + + const handleCancel = () => { + form.resetFields(); + setDomainCheckStatus(undefined); + onClose(); + }; + + return ( + +
+ + + + + + { + // Reset domain check status when user types + if (domainCheckStatus) { + setDomainCheckStatus(undefined); + form.setFields([ + { + name: 'domain', + errors: [], + }, + ]); + } + }} + /> + + + + + + + + + + + + + +
+ ); +}; + +export default DeploymentCreateModal; diff --git a/react/src/helper/index.tsx b/react/src/helper/index.tsx index 83db6dc9e4..39b410fc8b 100644 --- a/react/src/helper/index.tsx +++ b/react/src/helper/index.tsx @@ -9,6 +9,7 @@ import dayjs from 'dayjs'; import { Duration } from 'dayjs/plugin/duration'; import { TFunction } from 'i18next'; import _ from 'lodash'; +import { EnvVarFormListValue } from 'src/components/EnvVarFormList'; export const newLineToBrElement = ( text: string, @@ -613,3 +614,47 @@ export function listenToBackgroundTask< return controller.abort.bind(controller); } + +export const getAIAcceleratorWithStringifiedKey = (resourceSlot: any) => { + if (Object.keys(resourceSlot).length <= 0) { + return undefined; + } + const keyName: string = Object.keys(resourceSlot)[0]; + return { + acceleratorType: keyName, + accelerator: + typeof resourceSlot[keyName] === 'string' + ? keyName === 'cuda.shares' + ? parseFloat(resourceSlot[keyName]) + : parseInt(resourceSlot[keyName]) + : resourceSlot[keyName] || 0, + }; +}; + +// Helper functions for cleaner code +export const parseModelRuntimeConfig = (config: EnvVarFormListValue[]) => { + const parsed = _.isString(config) + ? _.attempt(() => JSON.parse(config || '{}')) || {} + : config || {}; + return _.toPairs(parsed).map(([variable, value]) => ({ + variable, + value: String(value), + })); +}; + +// Convert runtime config to JSON string for API +export const serializeModelRuntimeConfig = ( + config: EnvVarFormListValue[] | string | null | undefined, +): string | undefined => { + if (!config) return undefined; + // Convert array format [{variable, value}] to object format {variable: value} + const obj = _.isArray(config) + ? _.fromPairs( + config.map((v: { variable: string; value: string }) => [ + v.variable, + v.value, + ]), + ) + : _.attempt(() => JSON.parse(config || '{}')) || {}; + return obj ? JSON.stringify(obj) : undefined; +}; diff --git a/react/src/pages/DeploymentDetailPage.tsx b/react/src/pages/DeploymentDetailPage.tsx new file mode 100644 index 0000000000..fa2d51c450 --- /dev/null +++ b/react/src/pages/DeploymentDetailPage.tsx @@ -0,0 +1,175 @@ +import { PlusOutlined, EditOutlined, ReloadOutlined } from '@ant-design/icons'; +import { Card, Descriptions, Typography, Button, Table, Tag } from 'antd'; +import { ColumnType } from 'antd/lib/table'; +import { BAIFlex } from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { useWebUINavigate } from 'src/hooks'; + +const DeploymentDetailPage: React.FC = () => { + const { t } = useTranslation(); + const { deploymentId } = useParams<{ deploymentId: string }>(); + const webuiNavigate = useWebUINavigate(); + + // Get deployment data from mock data + // const deployment = mockDeployments.find((d) => d.id === deploymentId); + const deployment: any = undefined; + + if (!deployment) { + return
Deployment not found
; + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'ACTIVE': + return 'success'; + case 'INACTIVE': + return 'warning'; + default: + return 'default'; + } + }; + + const revisionColumns: ColumnType[] = [ + { + title: t('deployment.RevisionNumber'), + dataIndex: 'name', + key: 'name', + render: (name, row) => ( + + webuiNavigate(`/deployment/${deploymentId}/revision/${row.id}`) + } + > + {name} + + ), + }, + { + title: t('deployment.ImageName'), + dataIndex: ['image', 'id'], + key: 'image', + render: (imageId) => {imageId}, + }, + { + title: t('deployment.RuntimeVariant'), + dataIndex: ['modelRuntimeConfig', 'runtimeVariant'], + key: 'runtimeVariant', + render: (variant) => {variant}, + }, + { + title: t('deployment.MountDestination'), + dataIndex: ['modelMountConfig', 'mountDestination'], + key: 'mountDestination', + }, + { + title: t('deployment.CreatedAt'), + dataIndex: 'createdAt', + key: 'createdAt', + render: (date) => dayjs(date).format('ll LT'), + }, + ]; + + const descriptionsItems = [ + { + label: t('deployment.DeploymentName'), + children: ( + {deployment.metadata.name} + ), + }, + { + label: t('deployment.Domain'), + children: deployment.networkAccess.preferredDomainName, + }, + { + label: t('deployment.URL'), + children: ( + + {deployment.networkAccess.endpointUrl} + + ), + }, + { + label: t('deployment.CreatorEmail'), + children: deployment.createdUser.email, + }, + { + label: t('deployment.CreatedAt'), + children: dayjs(deployment.metadata.createdAt).format('ll LT'), + }, + { + label: t('deployment.Status'), + children: ( + + {deployment.metadata.status} + + ), + }, + ]; + + return ( + + + + {deployment.metadata.name} + + + + } + > + + + + + + + } + > +
edge.node, + )} + pagination={false} + scroll={{ x: 'max-content' }} + bordered + /> + + + ); +}; + +export default DeploymentDetailPage; diff --git a/react/src/pages/DeploymentLauncherPage.tsx b/react/src/pages/DeploymentLauncherPage.tsx new file mode 100644 index 0000000000..7906db6d70 --- /dev/null +++ b/react/src/pages/DeploymentLauncherPage.tsx @@ -0,0 +1,661 @@ +import { sanitizeSensitiveEnv } from '../components/EnvVarFormList'; +import ImageEnvironmentSelectFormItems, { + ImageEnvironmentFormInput, +} from '../components/ImageEnvironmentSelectFormItems'; +import { mainContentDivRefState } from '../components/MainLayout/MainLayout'; +import { + RESOURCE_ALLOCATION_INITIAL_FORM_VALUES, + ResourceAllocationFormValue, +} from '../components/SessionFormItems/ResourceAllocationFormItems'; +import { VFolderTableFormValues } from '../components/VFolderTableFormItem'; +import { getImageFullName, serializeModelRuntimeConfig } from '../helper'; +import { useWebUINavigate, useSuspendedBackendaiClient } from '../hooks'; +import { useSetBAINotification } from '../hooks/useBAINotification'; +import { useCurrentResourceGroupState } from '../hooks/useCurrentProject'; +import { + PlayCircleFilled, + LeftOutlined, + RightOutlined, + PlayCircleOutlined, + QuestionCircleOutlined, +} from '@ant-design/icons'; +import { useDebounceFn } from 'ahooks'; +import { + Button, + Card, + Form, + Grid, + StepProps, + Steps, + theme, + Popconfirm, + Tooltip, + Input, + Typography, +} from 'antd'; +import { BAIFlex, compareNumberWithUnits } from 'backend.ai-ui'; +import { useAtomValue } from 'jotai'; +import _ from 'lodash'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { graphql, useMutation } from 'react-relay'; +import { + DeploymentLauncherPageCreateModelDeploymentMutation, + DeploymentLauncherPageCreateModelDeploymentMutation$variables, +} from 'src/__generated__/DeploymentLauncherPageCreateModelDeploymentMutation.graphql'; +import DeploymentLauncherPreview from 'src/components/DeploymentLauncherPreview'; +import DeploymentMetadataFormItem from 'src/components/DeploymentMetadataFormItem'; +import DeploymentNetworkAccessFormItem from 'src/components/DeploymentNetworkAccessFormItem'; +import DeploymentRevisionRuntimeAndMountFormItem from 'src/components/DeploymentRevisionRuntimeAndMountFormItem'; +import DeploymentStrategyFormItem from 'src/components/DeploymentStrategyFormItem'; +import ResourceAllocationFormItems from 'src/components/SessionFormItems/ResourceAllocationFormItems'; +import { + JsonParam, + NumberParam, + StringParam, + useQueryParams, + withDefault, +} from 'use-query-params'; + +export type DeploymentLauncherFormValue = + DeploymentLauncherPageCreateModelDeploymentMutation$variables['input'] & + ImageEnvironmentFormInput & + ResourceAllocationFormValue & + VFolderTableFormValues; + +export type DeploymentLauncherStepKey = 'deployment' | 'revisions' | 'review'; + +interface StepPropsWithKey extends StepProps { + key: DeploymentLauncherStepKey; +} + +export const DEPLOYMENT_LAUNCHER_NOTI_PREFIX = 'deployment-launcher:'; + +// Common default values +export const DEFAULT_INITIAL_REVISION = { + name: undefined, + image: { name: undefined, architecture: undefined }, + modelRuntimeConfig: { + runtimeVariant: 'custom', + inferenceRuntimeConfig: undefined, + environ: undefined, + }, + modelMountConfig: { + vfolderId: '', + mountDestination: '', + definitionPath: '', + }, + extraMounts: [], +}; + +export const DEFAULT_ENVIRONMENTS = { + environment: '', + version: '', + image: undefined, + manual: '', + customizedTag: '', +}; + +interface DeploymentLauncherPageProps {} + +const DeploymentLauncherPage: React.FC = ({ + ...props +}) => { + const mainContentDivRef = useAtomValue(mainContentDivRefState); + + const [currentGlobalResourceGroup] = useCurrentResourceGroupState(); + const baiClient = useSuspendedBackendaiClient(); + + const webuiNavigate = useWebUINavigate(); + const { upsertNotification } = useSetBAINotification(); + + const [commitCreateModelDeployment, isInFlightCreateModelDeployment] = + useMutation(graphql` + mutation DeploymentLauncherPageCreateModelDeploymentMutation( + $input: CreateModelDeploymentInput! + ) { + createModelDeployment(input: $input) { + deployment { + id + # metadata { + # name + # status + # } + } + } + } + `); + + const StepParam = withDefault(NumberParam, 0); + const FormValuesParam = withDefault(JsonParam, {}); + const [ + { step: currentStep, formValues: formValuesFromQueryParams, redirectTo }, + setQuery, + ] = useQueryParams({ + step: StepParam, + formValues: FormValuesParam, + redirectTo: StringParam, + }); + + const deployment: any = undefined; + + // Build form values using lodash utilities + const INITIAL_FORM_VALUES = _.merge({ + // Base defaults + metadata: { name: '', tags: [] }, + networkAccess: { openToPublic: false }, + defaultDeploymentStrategy: { type: 'ROLLING' }, + desiredReplicaCount: 1, + initialRevision: DEFAULT_INITIAL_REVISION, + mount_ids: [], + mount_id_map: {}, + resourceGroup: currentGlobalResourceGroup, + ...RESOURCE_ALLOCATION_INITIAL_FORM_VALUES, + environments: baiClient?._config?.default_session_environment + ? _.assign({}, DEFAULT_ENVIRONMENTS, { + environment: baiClient._config.default_session_environment, + }) + : DEFAULT_ENVIRONMENTS, + allocationPreset: 'auto-select', + }); + + // Process URL params with simplified lodash transform + const processedFormValuesFromQueryParams = _.isEmpty( + formValuesFromQueryParams, + ) + ? {} + : _.transform( + formValuesFromQueryParams, + (result: any, value, key: string) => { + _.set(result, key, value); + }, + {}, + ); + + const mergedInitialValues = _.merge( + {}, + INITIAL_FORM_VALUES, + processedFormValuesFromQueryParams, + ); + + const { run: syncFormToURLWithDebounce } = useDebounceFn( + () => { + const currentValue = form.getFieldsValue(); + setQuery( + { + formValues: _.assign( + _.omit(form.getFieldsValue(), [ + 'environments.image', + 'environments.customizedTag', + 'initialRevision.modelRuntimeConfig.inferenceRuntimeConfig', + 'initialRevision.modelRuntimeConfig.environ', + ]), + { + 'initialRevision.modelRuntimeConfig.inferenceRuntimeConfig': + sanitizeSensitiveEnv( + currentValue?.initialRevision?.modelRuntimeConfig + ?.inferenceRuntimeConfig ?? [], + ), + 'initialRevision.modelRuntimeConfig.environ': + sanitizeSensitiveEnv( + _.isArray( + currentValue?.initialRevision?.modelRuntimeConfig?.environ, + ) + ? currentValue?.initialRevision?.modelRuntimeConfig?.environ + : JSON.parse( + currentValue?.initialRevision?.modelRuntimeConfig + ?.environ || '{}', + ), + ), + }, + ), + }, + 'replaceIn', + ); + }, + { + leading: false, + wait: 500, + trailing: true, + }, + ); + + const setCurrentStep = (nextStep: number) => { + setQuery( + { + step: nextStep, + }, + 'pushIn', + ); + }; + + const { token } = theme.useToken(); + const { t } = useTranslation(); + const screens = Grid.useBreakpoint(); + const [form] = Form.useForm(); + + // Validate form fields on mount + useEffect(() => { + form.validateFields().catch((e) => {}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ScrollTo top when step is changed + useEffect(() => { + mainContentDivRef.current?.scrollTo(0, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStep]); + + const steps: Array = [ + { + title: t('deployment.launcher.Deployment'), + key: 'deployment', + }, + { + title: t('deployment.launcher.InitialRevision'), + key: 'revisions', + }, + { + title: t('deployment.launcher.ConfirmAndDeploy'), + icon: , + key: 'review', + }, + ]; + + const currentStepKey = steps[currentStep]?.key; + + const hasError = _.some( + form.getFieldsError(), + (item) => item.errors.length > 0, + ); + + const createDeployment = () => { + form + .validateFields() + .then(async (values) => { + const deploymentInput: DeploymentLauncherPageCreateModelDeploymentMutation$variables['input'] = + { + metadata: { + name: values.metadata.name, + tags: values.metadata.tags, + domainName: values.metadata.domainName, + projectId: values.metadata.projectId, + }, + networkAccess: { + preferredDomainName: values.networkAccess.preferredDomainName, + openToPublic: values.networkAccess.openToPublic, + }, + defaultDeploymentStrategy: { + type: values.defaultDeploymentStrategy.type, + }, + desiredReplicaCount: values.desiredReplicaCount, + initialRevision: { + name: values.initialRevision.name, + image: { + name: + values.environments.manual || + getImageFullName(values.environments.image) || + values.initialRevision.image.name, + architecture: + values.environments.image?.architecture || + values.initialRevision.image.architecture || + 'x86_64', + }, + modelRuntimeConfig: { + runtimeVariant: + values.initialRevision.modelRuntimeConfig.runtimeVariant, + inferenceRuntimeConfig: serializeModelRuntimeConfig( + values.initialRevision.modelRuntimeConfig + ?.inferenceRuntimeConfig, + ), + environ: serializeModelRuntimeConfig( + values.initialRevision.modelRuntimeConfig?.environ, + ), + }, + modelMountConfig: { + ...values.initialRevision.modelMountConfig, + mountDestination: + values.initialRevision.modelMountConfig.mountDestination || + '/models', + definitionPath: + values.initialRevision.modelMountConfig.definitionPath || + 'model-definition.yaml', + }, + extraMounts: + values.mount_ids?.map((vfolderId) => ({ + vfolderId: vfolderId, + mountDestination: + values.mount_id_map?.[vfolderId] || + `/home/work/${vfolderId}`, + })) || [], + clusterConfig: { + mode: + values.cluster_mode === 'single-node' + ? 'SINGLE_NODE' + : 'MULTI_NODE', + size: values.cluster_size, + }, + resourceConfig: { + resourceGroup: { + name: values.resourceGroup, + }, + resourceSlots: JSON.stringify({ + cpu: values.resource.cpu, + mem: values.resource.mem, + ...(values.resource.accelerator > 0 + ? { + [values.resource.acceleratorType]: + values.resource.accelerator, + } + : undefined), + }), + resourceOpts: values.resource.shmem + ? JSON.stringify({ + shmem: + compareNumberWithUnits(values.resource.mem, '4g') > 0 && + compareNumberWithUnits(values.resource.shmem, '1g') < 0 + ? '1g' + : values.resource.shmem, + }) + : undefined, + }, + }, + }; + + const deploymentName = deploymentInput.metadata.name; + + commitCreateModelDeployment({ + variables: { input: deploymentInput }, + onCompleted: (res, errors) => { + if (!res?.createModelDeployment?.deployment) { + upsertNotification({ + key: DEPLOYMENT_LAUNCHER_NOTI_PREFIX + deploymentName, + message: t('deployment.launcher.DeploymentFailed'), + description: t('deployment.launcher.UnknownError'), + type: 'error', + duration: 0, + open: true, + }); + return; + } + if (errors && errors?.length > 0) { + const errorMsgList = _.map(errors, (error) => error.message); + for (const error of errorMsgList) { + upsertNotification({ + key: + DEPLOYMENT_LAUNCHER_NOTI_PREFIX + deploymentName + '_error', + message: t('deployment.launcher.DeploymentFailed'), + description: error, + type: 'error', + duration: 0, + open: true, + }); + } + } else { + console.log('Deployment created successfully:', res); + + upsertNotification({ + key: DEPLOYMENT_LAUNCHER_NOTI_PREFIX + deploymentName, + message: t('deployment.launcher.DeploymentCreated', { + name: deploymentName, + }), + description: t( + 'deployment.launcher.DeploymentCreatedDescription', + ), + duration: 5, + open: true, + }); + + webuiNavigate(redirectTo || '/deployment'); + } + }, + onError: (err) => { + console.error('Failed to create deployment:', err); + upsertNotification({ + key: DEPLOYMENT_LAUNCHER_NOTI_PREFIX + deploymentName, + message: t('deployment.launcher.DeploymentFailed'), + description: + err?.message || t('deployment.launcher.UnknownError'), + type: 'error', + duration: 0, + open: true, + }); + }, + }); + }) + .catch((e) => { + console.log('validation errors', e); + }); + }; + + const [isQueryReset, setIsQueryReset] = useState(false); + useLayoutEffect(() => { + if (isQueryReset) { + form.resetFields(); + setIsQueryReset(false); + } + }, [isQueryReset, form]); + + const handleNext = () => { + setCurrentStep(currentStep + 1); + }; + + const handlePrevious = () => { + setCurrentStep(currentStep - 1); + }; + + return ( + + + + + + {deployment + ? t('deployment.launcher.UpdateDeployment') + : t('deployment.launcher.CreateNewDeployment')} + + + + { + syncFormToURLWithDebounce(); + }} + > +
+ + {/* Step 1: Metadata */} + + + + + {/* Step 1: Deployment Strategy */} + + + + + {/* Step 1: Network Access */} + + + + + {/* Step 2: Initial Revision - Name and Environments */} + + + + + + + + {/* modelRuntimeConfig, modelMountConfig and extraMounts */} + + + {/* clusterConfig and resourceConfig */} + + + + + {/* Step 3: Review and Confirm */} + {currentStepKey === 'review' && ( + { + const stepIndex = steps.findIndex( + (step) => step.key === stepKey, + ); + if (stepIndex !== -1) { + setCurrentStep(stepIndex); + } + }} + /> + )} + + {/* Navigation Buttons */} + + + { + setQuery({}, 'replace'); + setIsQueryReset(true); + }} + icon={ + + } + okText={t('button.Reset')} + okButtonProps={{ + danger: true, + }} + > + + + + + {currentStep > 0 && ( + + )} + {currentStep === steps.length - 1 ? ( + + + + ) : ( + + )} + + + + +
+
+ + {screens.lg && ( + + { + setCurrentStep(nextCurrent); + }} + items={_.map(steps, (s, idx) => ({ + ...s, + status: idx === currentStep ? 'process' : 'wait', + }))} + /> + + )} +
+
+ ); +}; + +export default DeploymentLauncherPage; diff --git a/react/src/pages/DeploymentListPage.tsx b/react/src/pages/DeploymentListPage.tsx index c92ca6eb99..1d88450b40 100644 --- a/react/src/pages/DeploymentListPage.tsx +++ b/react/src/pages/DeploymentListPage.tsx @@ -1,6 +1,6 @@ import BAIFetchKeyButton from '../components/BAIFetchKeyButton'; import DeploymentList from '../components/DeploymentList'; -import { INITIAL_FETCH_KEY, useFetchKey } from '../hooks'; +import { INITIAL_FETCH_KEY, useFetchKey, useWebUINavigate } from '../hooks'; import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions'; import { useDeferredQueryParams } from '../hooks/useDeferredQueryParams'; import { Button } from 'antd'; @@ -20,6 +20,7 @@ import { StringParam, withDefault } from 'use-query-params'; const DeploymentListPage: React.FC = () => { const { t } = useTranslation(); + const webuiNavigate = useWebUINavigate(); const { tablePaginationOption, setTablePaginationOption } = useBAIPaginationOptionStateOnSearchParam({ @@ -97,7 +98,12 @@ const DeploymentListPage: React.FC = () => { updateFetchKey(newFetchKey); }} /> - + } styles={{ diff --git a/react/src/pages/Deployments/DeploymentCreatePage.tsx b/react/src/pages/Deployments/DeploymentCreatePage.tsx new file mode 100644 index 0000000000..6b21c3b3b8 --- /dev/null +++ b/react/src/pages/Deployments/DeploymentCreatePage.tsx @@ -0,0 +1,159 @@ +import { useWebUINavigate } from '../../hooks'; +import { ExclamationCircleOutlined } from '@ant-design/icons'; +import { + Form, + Input, + Select, + Button, + Card, + Typography, + Alert, + Space, +} from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface DeploymentCreateFormValues { + name: string; + domain?: string; + mode: 'simple' | 'expert'; +} + +const DeploymentCreatePage: React.FC = () => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const webuiNavigate = useWebUINavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (values: DeploymentCreateFormValues) => { + setIsSubmitting(true); + try { + // Mock API call - replace with actual implementation + console.log('Creating deployment:', values); + + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Navigate to deployment detail page after creation + webuiNavigate(`/deployment/mock-id`); + } catch (error) { + console.error('Failed to create deployment:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + webuiNavigate('/deployment'); + }; + + return ( + + + {t('deployment.CreateDeployment')} + + + +
+ + + + + + + + + +