From 11a2637db6d4e0e9cf6083684fb89fc5015d72ca Mon Sep 17 00:00:00 2001 From: Jialei <3217223+jialeicui@users.noreply.github.com> Date: Fri, 14 Jul 2023 11:43:24 +0800 Subject: [PATCH] chore(controller): refine the exposed links (#2504) --- .../src/domain/job/components/ExposedLink.tsx | 26 ++++++++++++++ console/src/domain/job/schemas/job.tsx | 13 ++++++- console/src/pages/Job/JobListCard.tsx | 20 +++-------- .../mlops/api/protocol/job/ExposedLinkVo.java | 29 +++++++++++++++ .../mlops/api/protocol/job/JobVo.java | 2 +- .../domain/job/converter/JobConverter.java | 36 ++++++++++++------- .../mlops/domain/job/step/ExposedType.java | 22 ++++++++++++ .../job/converter/JobConverterTest.java | 8 +++-- 8 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 console/src/domain/job/components/ExposedLink.tsx create mode 100644 server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/ExposedLinkVo.java create mode 100644 server/controller/src/main/java/ai/starwhale/mlops/domain/job/step/ExposedType.java diff --git a/console/src/domain/job/components/ExposedLink.tsx b/console/src/domain/job/components/ExposedLink.tsx new file mode 100644 index 0000000000..eec4525a67 --- /dev/null +++ b/console/src/domain/job/components/ExposedLink.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { ExposedLinkType, IExposedLinkSchema } from '@job/schemas/job' +import IconFont, { IconTypesT } from '@starwhale/ui/IconFont' + +export interface IExposedLinkProps { + data: IExposedLinkSchema +} + +const VSCODE_NAME = 'VS_CODE' + +function ExposedLink({ data: { type, name, link } }: IExposedLinkProps) { + let font: IconTypesT = 'global' + let title = name + if (type === ExposedLinkType.DEV_MODE && name === VSCODE_NAME) { + font = 'vscode' + title = 'vscode' + } + + return ( + + + + ) +} + +export default ExposedLink diff --git a/console/src/domain/job/schemas/job.tsx b/console/src/domain/job/schemas/job.tsx index 5b99da1d38..182f53ebd2 100644 --- a/console/src/domain/job/schemas/job.tsx +++ b/console/src/domain/job/schemas/job.tsx @@ -22,6 +22,17 @@ export enum JobStatusType { UNKNOWN = 'UNKNOWN', } +export enum ExposedLinkType { + DEV_MODE = 'DEV_MODE', + WEB_HANDLER = 'WEB_HANDLER', +} + +export interface IExposedLinkSchema { + type: ExposedLinkType + name: string + link: string +} + export interface IJobSchema extends IResourceSchema { uuid: string name: string @@ -39,7 +50,7 @@ export interface IJobSchema extends IResourceSchema { stopTime?: number createdTime?: number pinnedTime?: number - exposedLinks?: string[] + exposedLinks?: IExposedLinkSchema[] isTimeToLiveInSec?: boolean timeToLiveInSec?: number } diff --git a/console/src/pages/Job/JobListCard.tsx b/console/src/pages/Job/JobListCard.tsx index 13b51bec93..7040f077d4 100644 --- a/console/src/pages/Job/JobListCard.tsx +++ b/console/src/pages/Job/JobListCard.tsx @@ -18,10 +18,10 @@ import JobStatus from '@/domain/job/components/JobStatus' import Button from '@starwhale/ui/Button' import { useAuthPrivileged, WithCurrentAuth } from '@/api/WithAuth' import { IconTooltip } from '@starwhale/ui/Tooltip' -import IconFont from '@starwhale/ui/IconFont' import { useProjectRole } from '@project/hooks/useProjectRole' import { ConfigurationOverride } from '@starwhale/ui/base/helpers/overrides' import { ConfirmButton } from '@starwhale/ui' +import ExposedLink from '@job/components/ExposedLink' interface IActionButtonProps { jobId: string @@ -226,21 +226,11 @@ export default function JobListCard() { typeof job.duration === 'string' ? '-' : durationToStr(job.duration), job?.stopTime && job?.stopTime > 0 ? formatTimestampDateTime(job?.stopTime) : '-', , -
+
{actions[job.jobStatus] ?? ''} - {job.exposedLinks?.map((link) => { - return ( - - - - ) - })} + {job.exposedLinks?.map((exposed) => ( + + ))}
, ] }) ?? [] diff --git a/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/ExposedLinkVo.java b/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/ExposedLinkVo.java new file mode 100644 index 0000000000..9d5c268591 --- /dev/null +++ b/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/ExposedLinkVo.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Starwhale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.starwhale.mlops.api.protocol.job; + +import ai.starwhale.mlops.domain.job.step.ExposedType; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ExposedLinkVo { + private ExposedType type; + private String name; + private String link; +} diff --git a/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/JobVo.java b/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/JobVo.java index 404c82d2e7..8069c950cd 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/JobVo.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/api/protocol/job/JobVo.java @@ -82,7 +82,7 @@ public class JobVo implements Serializable { // expose links is used to get the serving url of the model, it may contain: // 1. vscode url when the model is running under dev mode // 2. serving url when the model is running a web handler (which using the non-zero expose handler decorator) - private List exposedLinks; + private List exposedLinks; @JsonProperty("duration") public Long getDuration() { diff --git a/server/controller/src/main/java/ai/starwhale/mlops/domain/job/converter/JobConverter.java b/server/controller/src/main/java/ai/starwhale/mlops/domain/job/converter/JobConverter.java index 7e392f3c6e..14e1a66ff7 100644 --- a/server/controller/src/main/java/ai/starwhale/mlops/domain/job/converter/JobConverter.java +++ b/server/controller/src/main/java/ai/starwhale/mlops/domain/job/converter/JobConverter.java @@ -17,6 +17,7 @@ package ai.starwhale.mlops.domain.job.converter; +import ai.starwhale.mlops.api.protocol.job.ExposedLinkVo; import ai.starwhale.mlops.api.protocol.job.JobVo; import ai.starwhale.mlops.api.protocol.runtime.RuntimeVo; import ai.starwhale.mlops.api.protocol.user.UserVo; @@ -24,12 +25,14 @@ import ai.starwhale.mlops.common.proxy.WebServerInTask; import ai.starwhale.mlops.domain.dataset.DatasetDao; import ai.starwhale.mlops.domain.dataset.bo.DatasetVersion; +import ai.starwhale.mlops.domain.job.DevWay; import ai.starwhale.mlops.domain.job.bo.Job; import ai.starwhale.mlops.domain.job.cache.HotJobHolder; import ai.starwhale.mlops.domain.job.po.JobEntity; import ai.starwhale.mlops.domain.job.spec.JobSpecParser; import ai.starwhale.mlops.domain.job.spec.StepSpec; import ai.starwhale.mlops.domain.job.status.JobStatus; +import ai.starwhale.mlops.domain.job.step.ExposedType; import ai.starwhale.mlops.domain.runtime.RuntimeService; import ai.starwhale.mlops.domain.system.SystemSettingService; import ai.starwhale.mlops.domain.task.bo.Task; @@ -40,12 +43,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import java.util.ArrayList; import java.util.List; -import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import reactor.function.Consumer4; @Component public class JobConverter { @@ -99,38 +102,38 @@ private List findDatasetVersionNamesByJobId(Long jobId) { * @param jobId job id * @return exposed links */ - private List generateJobExposedLinks(Long jobId) { - var links = new ArrayList(); + private List generateJobExposedLinks(Long jobId) { + var exposed = new ArrayList(); var jobs = hotJobHolder.ofIds(List.of(jobId)); if (CollectionUtils.isEmpty(jobs)) { - return links; + return exposed; } var job = jobs.stream().findAny().get(); // only running job should generate exposed links var jobStatus = job.getStatus(); if (jobStatus != JobStatus.RUNNING) { - return links; + return exposed; } // check if job has exposed port in step spec var stepSpecStr = job.getStepSpec(); if (!StringUtils.hasText(stepSpecStr)) { - return links; + return exposed; } List stepSpecs; try { stepSpecs = jobSpecParser.parseAndFlattenStepFromYaml(stepSpecStr); } catch (JsonProcessingException e) { - return links; + return exposed; } if (CollectionUtils.isEmpty(stepSpecs)) { - return links; + return exposed; } var steps = job.getSteps(); - BiConsumer addRunningTask = (task, port) -> { + Consumer4 addRunningTask = (task, port, type, name) -> { if (task.getStatus() != TaskStatus.RUNNING) { return; } @@ -139,13 +142,13 @@ private List generateJobExposedLinks(Long jobId) { return; } var link = webServerInTask.generateGatewayUrl(task.getId(), task.getIp(), port); - links.add(link); + exposed.add(ExposedLinkVo.builder().type(type).name(name).link(link).build()); }; // dev mode if (job.isDevMode()) { job.getSteps().stream().flatMap(s -> s.getTasks().stream()) - .forEach(task -> addRunningTask.accept(task, devPort)); + .forEach(task -> addRunningTask.accept(task, devPort, ExposedType.DEV_MODE, DevWay.VS_CODE.name())); } // web handler @@ -165,10 +168,17 @@ private List generateJobExposedLinks(Long jobId) { if (CollectionUtils.isEmpty(tasks)) { return; } - tasks.forEach(task -> addRunningTask.accept(task, exposedPort)); + String name; + // https://github.com/star-whale/starwhale/blob/c924313166d065ab941a99fa1dec04b7bfbe5fa7/client/starwhale/core/model/model.py#L246-L252 + if (Boolean.TRUE.equals(stepSpec.getVirtual()) && stepSpec.getName().equals("serving")) { + name = "online evaluation"; + } else { + name = stepSpec.getName(); + } + tasks.forEach(t -> addRunningTask.accept(t, exposedPort, ExposedType.WEB_HANDLER, name)); }); - return links; + return exposed; } public JobVo convert(Job job) throws ConvertException { diff --git a/server/controller/src/main/java/ai/starwhale/mlops/domain/job/step/ExposedType.java b/server/controller/src/main/java/ai/starwhale/mlops/domain/job/step/ExposedType.java new file mode 100644 index 0000000000..4de1bfc66a --- /dev/null +++ b/server/controller/src/main/java/ai/starwhale/mlops/domain/job/step/ExposedType.java @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Starwhale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.starwhale.mlops.domain.job.step; + +public enum ExposedType { + DEV_MODE, + WEB_HANDLER, +} diff --git a/server/controller/src/test/java/ai/starwhale/mlops/domain/job/converter/JobConverterTest.java b/server/controller/src/test/java/ai/starwhale/mlops/domain/job/converter/JobConverterTest.java index 06403116c0..d0ed598b0b 100644 --- a/server/controller/src/test/java/ai/starwhale/mlops/domain/job/converter/JobConverterTest.java +++ b/server/controller/src/test/java/ai/starwhale/mlops/domain/job/converter/JobConverterTest.java @@ -28,6 +28,7 @@ import static org.mockito.BDDMockito.mock; import static org.mockito.Mockito.when; +import ai.starwhale.mlops.api.protocol.job.ExposedLinkVo; import ai.starwhale.mlops.api.protocol.runtime.RuntimeVo; import ai.starwhale.mlops.api.protocol.user.UserVo; import ai.starwhale.mlops.common.IdConverter; @@ -39,6 +40,7 @@ import ai.starwhale.mlops.domain.job.spec.JobSpecParser; import ai.starwhale.mlops.domain.job.spec.StepSpec; import ai.starwhale.mlops.domain.job.status.JobStatus; +import ai.starwhale.mlops.domain.job.step.ExposedType; import ai.starwhale.mlops.domain.job.step.bo.Step; import ai.starwhale.mlops.domain.model.po.ModelVersionEntity; import ai.starwhale.mlops.domain.runtime.RuntimeService; @@ -166,13 +168,15 @@ public void testConvertJobExposedLinks() throws JsonProcessingException { entity.setJobStatus(JobStatus.RUNNING); job.setStatus(JobStatus.RUNNING); res = jobConvertor.convert(entity); - assertThat(res.getExposedLinks(), is(List.of("/foo/bar/"))); + var expectWeb = ExposedLinkVo.builder().type(ExposedType.WEB_HANDLER).link("/foo/bar/").name("step").build(); + assertThat(res.getExposedLinks(), is(List.of(expectWeb))); // get debug mode links when dev mode is on when(webServerInTask.generateGatewayUrl(7L, "1.1.1.1", 1234)).thenReturn("/baz/"); entity.setDevMode(true); job.setDevMode(true); res = jobConvertor.convert(entity); - assertThat(res.getExposedLinks(), is(List.of("/baz/", "/foo/bar/"))); + var expectDevMode = ExposedLinkVo.builder().type(ExposedType.DEV_MODE).link("/baz/").name("VS_CODE").build(); + assertThat(res.getExposedLinks(), is(List.of(expectDevMode, expectWeb))); } }