diff --git a/client/scripts/update_api_spec.js b/client/scripts/update_api_spec.js index ae036591de..d2ae8e1eb9 100644 --- a/client/scripts/update_api_spec.js +++ b/client/scripts/update_api_spec.js @@ -24,7 +24,7 @@ import { parseDocument } from "yaml"; const GH_BASE_URL = "https://raw.githubusercontent.com"; const DATA_SERVICES_REPO = "SwissDataScienceCenter/renku-data-services"; -const DATA_SERVICES_RELEASE = "leafty/feat-resource-class-platforms"; +const DATA_SERVICES_RELEASE = "build/support-build-arm"; async function main() { argv.forEach((arg) => { diff --git a/client/src/features/sessionsV2/SessionImageModal.tsx b/client/src/features/sessionsV2/SessionImageModal.tsx index b87b6f6232..e19cb0285a 100644 --- a/client/src/features/sessionsV2/SessionImageModal.tsx +++ b/client/src/features/sessionsV2/SessionImageModal.tsx @@ -76,7 +76,7 @@ export default function SessionImageModal({ ) : ( <>
- +
{!data.connection && !data.provider ? ( <> diff --git a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx index 18d0b1b3c5..808b630105 100644 --- a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx @@ -18,7 +18,7 @@ import { skipToken } from "@reduxjs/toolkit/query"; import cx from "classnames"; -import { useContext } from "react"; +import { useContext, useMemo } from "react"; import { CircleFill, Link45deg, Pencil, Trash } from "react-bootstrap-icons"; import { Card, CardBody, Col, DropdownItem, Row } from "reactstrap"; @@ -29,6 +29,7 @@ import { DEFAULT_APP_PARAMS } from "../../../utils/context/appParams.constants"; import PermissionsGuard from "../../permissionsV2/PermissionsGuard"; import useProjectPermissions from "../../ProjectPageV2/utils/useProjectPermissions.hook"; import { Project } from "../../projectsV2/api/projectV2.api"; +import { computeResourcesApi } from "../api/computeResources.api"; import type { SessionLauncher } from "../api/sessionLaunchersV2.api"; import { sessionLaunchersV2Api, @@ -127,15 +128,24 @@ export default function SessionLauncherCard({ "text-muted", ]; - const { data: containerImage, isLoading: loadingContainerImage } = + const { data: containerImage, isLoading: isLoadingContainerImage } = useGetSessionsImagesQuery( - environment && - environment.environment_kind === "CUSTOM" && - environment.container_image + environment?.container_image != null ? { imageUrl: environment.container_image } : skipToken ); + const { data: resourcePools, isLoading: isLoadingResourcePools } = + computeResourcesApi.endpoints.getResourcePools.useQueryState({}); + const resourcePool = useMemo(() => { + if (launcher?.resource_class_id == null || resourcePools == null) { + return undefined; + } + return resourcePools.find(({ classes }) => + classes.some(({ id }) => id === launcher.resource_class_id) + ); + }, [launcher?.resource_class_id, resourcePools]); + return ( - {isCodeEnvironment && ( + {isCodeEnvironment ? ( - {isCodeEnvironment && isLoading ? ( + {isCodeEnvironment && + (isLoading || + isLoadingContainerImage || + isLoadingResourcePools) ? ( @@ -220,7 +233,11 @@ export default function SessionLauncherCard({ ) : isCodeEnvironment && lastBuild ? ( - + ) : !hasSession ? ( - )} - {isExternalImageEnvironment && ( + ) : ( diff --git a/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx b/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx index 26759f4853..965431306b 100644 --- a/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx +++ b/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx @@ -32,6 +32,7 @@ import AppContext from "../../../utils/context/appContext"; import { DEFAULT_APP_PARAMS } from "../../../utils/context/appParams.constants"; import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook"; import { toHumanDateTime } from "../../../utils/helpers/DateTimeUtils"; +import { computeResourcesApi } from "../api/computeResources.api"; import type { SessionLauncher } from "../api/sessionLaunchersV2.api"; import { sessionLaunchersV2Api, @@ -115,6 +116,7 @@ export default function EnvironmentCard({ {environment_kind === "GLOBAL" && ( <> + {environment?.description ? (

@@ -150,11 +152,45 @@ export default function EnvironmentCard({ ); } +function GlobalEnvironmentSessionImageBadge({ + launcher, +}: { + launcher: SessionLauncher; +}) { + const environment = launcher.environment; + const { data, isLoading } = useGetSessionsImagesQuery( + environment && environment.container_image + ? { imageUrl: environment.container_image } + : skipToken + ); + const { data: resourcePools, isLoading: isLoadingResourcePools } = + computeResourcesApi.endpoints.getResourcePools.useQueryState({}); + const resourcePool = useMemo(() => { + if (launcher?.resource_class_id == null || resourcePools == null) { + return undefined; + } + return resourcePools.find(({ classes }) => + classes.some(({ id }) => id === launcher.resource_class_id) + ); + }, [launcher?.resource_class_id, resourcePools]); + + return ( +

+ +
+ ); +} + function CustomEnvironmentValues({ launcher }: { launcher: SessionLauncher }) { const { environment } = launcher; if (environment.environment_image_source === "image") { - return ; + return ; } return ; @@ -162,8 +198,10 @@ function CustomEnvironmentValues({ launcher }: { launcher: SessionLauncher }) { function CustomImageEnvironmentValues({ launcher, + showImageBadge, }: { launcher: SessionLauncher; + showImageBadge?: boolean; }) { const { pathname, hash } = useLocation(); const environment = launcher.environment; @@ -175,6 +213,16 @@ function CustomImageEnvironmentValues({ ? { imageUrl: environment.container_image } : skipToken ); + const { data: resourcePools, isLoading: isLoadingResourcePools } = + computeResourcesApi.endpoints.getResourcePools.useQueryState({}); + const resourcePool = useMemo(() => { + if (launcher?.resource_class_id == null || resourcePools == null) { + return undefined; + } + return resourcePools.find(({ classes }) => + classes.some(({ id }) => id === launcher.resource_class_id) + ); + }, [launcher?.resource_class_id, resourcePools]); const search = useMemo(() => { return `?${new URLSearchParams({ targetProvider: data?.provider?.id ?? "", @@ -188,7 +236,14 @@ function CustomImageEnvironmentValues({ return ( <>
- + {showImageBadge && ( + + )} {!isLoading && data?.accessible === false && (
{!data.connection && !data.provider ? ( @@ -336,6 +391,23 @@ function CustomBuildEnvironmentValues({ } ); + const { data: imageCheck, isLoading: isLoadingContainerImage } = + useGetSessionsImagesQuery( + environment.container_image != null + ? { imageUrl: environment.container_image } + : skipToken + ); + const { data: resourcePools, isLoading: isLoadingResourcePools } = + computeResourcesApi.endpoints.getResourcePools.useQueryState({}); + const resourcePool = useMemo(() => { + if (launcher?.resource_class_id == null || resourcePools == null) { + return undefined; + } + return resourcePools.find(({ classes }) => + classes.some(({ id }) => id === launcher.resource_class_id) + ); + }, [launcher?.resource_class_id, resourcePools]); + // Invalidate launchers if the container image is not the same as the // image from the last successful build const dispatch = useAppDispatch(); @@ -368,7 +440,12 @@ function CustomBuildEnvironmentValues({ ) : ( <> - + {lastSuccessfulBuild && ( - +
)} @@ -505,25 +582,6 @@ function EnvironmentJSONArrayRowWithLabel({ ); } -function ReadyStatusBadge() { - return ( - - - Ready - - ); -} - function NotReadyStatusBadge() { return ( { + if (imageCheck == null || resourcePool == null) { + return "unknown"; + } + return isImageCompatibleWith(imageCheck, resourcePool.platform); + }, [imageCheck, resourcePool]); + const badgeIcon = - status === "in_progress" ? ( + buildStatus === "in_progress" ? ( ) : ( ); const badgeText = - status === "in_progress" + isCompatible === false + ? "Image incompatible" + : buildStatus === "in_progress" ? "Build in progress" - : status === "cancelled" + : buildStatus === "cancelled" ? "Build cancelled" - : status === "succeeded" + : buildStatus === "succeeded" ? "Build succeeded" : "Build failed"; const badgeColorClasses = - status === "in_progress" + isCompatible === false + ? ["border-danger", "bg-danger-subtle", "text-danger-emphasis"] + : buildStatus === "in_progress" ? ["border-warning", "bg-warning-subtle", "text-warning-emphasis"] - : status === "succeeded" + : buildStatus === "succeeded" ? ["border-success", "bg-success-subtle", "text-success-emphasis"] : ["border-danger", "bg-danger-subtle", "text-danger-emphasis"]; diff --git a/client/src/features/sessionsV2/components/SessionStatus/SessionImageBadge.tsx b/client/src/features/sessionsV2/components/SessionStatus/SessionImageBadge.tsx index 7f2eaead09..6f9e3820e8 100644 --- a/client/src/features/sessionsV2/components/SessionStatus/SessionImageBadge.tsx +++ b/client/src/features/sessionsV2/components/SessionStatus/SessionImageBadge.tsx @@ -17,26 +17,43 @@ */ import cx from "classnames"; +import { useMemo } from "react"; import { CircleFill } from "react-bootstrap-icons"; import { Loader } from "~/components/Loader"; import RenkuBadge from "~/components/renkuBadge/RenkuBadge"; -import { ImageCheckResponse } from "../../api/sessionsV2.generated-api"; +import type { ResourcePoolWithId } from "../../api/computeResources.api"; +import type { ImageCheckResponse } from "../../api/sessionsV2.api"; +import { isImageCompatibleWith } from "../../session.utils"; interface SessionImageBadgeProps { data?: ImageCheckResponse | null; - loading: boolean; + isLoading: boolean; + + resourcePool?: ResourcePoolWithId; + isLoadingResourcePools?: boolean; } export default function SessionImageBadge({ data, - loading, + isLoading, + resourcePool, + isLoadingResourcePools, }: SessionImageBadgeProps) { + const isCompatible = useMemo(() => { + if (data == null || resourcePool == null) { + return "unknown"; + } + return isImageCompatibleWith(data, resourcePool.platform); + }, [data, resourcePool]); + return ( - {loading ? ( + {isLoading || isLoadingResourcePools ? ( <> Checking image status. @@ -55,7 +72,11 @@ export default function SessionImageBadge({ ) : ( <> - {data?.accessible + {isCompatible === false + ? `Image incompatible${ + resourcePool?.platform ? ` with ${resourcePool.platform}` : "" + }` + : data?.accessible ? "Image accessible" : data?.provider?.id && (!data?.connection || data?.connection?.status !== "connected") diff --git a/client/src/features/sessionsV2/session.utils.ts b/client/src/features/sessionsV2/session.utils.ts index 00ea9d3130..a0577a9673 100644 --- a/client/src/features/sessionsV2/session.utils.ts +++ b/client/src/features/sessionsV2/session.utils.ts @@ -18,12 +18,14 @@ import { FaviconStatus } from "../display/display.types"; import { SessionStatusState } from "../session/sessions.types"; +import type { ResourcePoolWithId } from "./api/computeResources.api"; import type { EnvironmentList as SessionEnvironmentList, SessionLauncher, SessionLauncherEnvironmentParams, SessionLauncherEnvironmentPatchParams, } from "./api/sessionLaunchersV2.api"; +import type { ImageCheckResponse } from "./api/sessionsV2.api"; import { BUILDER_PLATFORMS, DEFAULT_URL, @@ -402,3 +404,16 @@ export function validateEnvVariableName(name: string): true | string { } return true; } + +export function isImageCompatibleWith( + image: ImageCheckResponse, + platform: ResourcePoolWithId["platform"] +): boolean | "unknown" { + if (image.platforms == null) { + return "unknown"; + } + const imagePlatforms = image.platforms?.map( + ({ os, architecture }) => `${os}/${architecture}` + ); + return imagePlatforms.some((p) => p === platform); +}