diff --git a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx index 2d63ee4a57..122a52f067 100644 --- a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx @@ -19,10 +19,11 @@ import { skipToken } from "@reduxjs/toolkit/query"; import cx from "classnames"; import { useContext, useMemo } from "react"; -import { CircleFill, Link45deg, Pencil, Trash } from "react-bootstrap-icons"; +import { Link45deg, Pencil, Trash } from "react-bootstrap-icons"; import { Card, CardBody, Col, DropdownItem, Row } from "reactstrap"; import SessionEnvironmentGitLabWarningBadge from "~/features/legacy/SessionEnvironmentGitLabWarnBadge"; +import { useGetRepositoryQuery } from "~/features/repositories/api/repositories.api"; import { Loader } from "../../../components/Loader"; import AppContext from "../../../utils/context/appContext"; import { DEFAULT_APP_PARAMS } from "../../../utils/context/appParams.constants"; @@ -135,6 +136,15 @@ export default function SessionLauncherCard({ : skipToken ); + const { + data: imageRepositorySource, + isLoading: isLoadingImageRepositorySource, + } = useGetRepositoryQuery( + environment?.environment_image_source === "build" + ? { url: environment.build_parameters.repository } + : skipToken + ); + const { data: resourcePools, isLoading: isLoadingResourcePools } = computeResourcesApi.endpoints.getResourcePools.useQueryState({}); // Ref: https://github.com/facebook/react/issues/35577 @@ -221,6 +231,7 @@ export default function SessionLauncherCard({ {isCodeEnvironment && (isLoading || isLoadingContainerImage || + isLoadingImageRepositorySource || isLoadingResourcePools) ? ( - ) : !hasSession ? ( - - - - Not Running - - - ) : null} + ) : ( + + )} { @@ -248,82 +257,91 @@ function CustomImageEnvironmentValues({ isLoadingResourcePools={isLoadingResourcePools} /> )} - {!isLoading && data?.accessible === false && ( -
- {!data.connection && !data.provider ? ( - -

- The container image reference is invalid or points to an - unsupported registry. Please verify the image and check if the - registry is in the currently supported{" "} - - - integrations - - . If you're certain the image is correct and points to a - registry we don't currently support,{" "} - - - contact us - {" "} - about adding an integration. -

-
- ) : data.connection?.status === "connected" ? ( - -

- Either the container image reference does not exist, or you do - not have access to it. -

- {data?.provider?.id && ( - <> -

- If you think you should have access, check your - integration configuration. -

+ {!isLoading && + data?.accessible === false && + !isLoadingImageRepositorySource && ( +
+ {imageRepositorySource?.status === "invalid" ? ( + +

+ You do not have access to the repository used to build the + image for this session environment. +

+
+ ) : !data.connection && !data.provider ? ( + +

+ The container image reference is invalid or points to an + unsupported registry. Please verify the image and check if + the registry is in the currently supported{" "} - View integration + integrations - - )} - - ) : ( - -

- This container image reference is from a supported registry, - but you haven't activated the integration yet. Activate - the integration to check if you have access to this image. -

- - - Go to Integration - - - )} -
- )} + . If you're certain the image is correct and points to + a registry we don't currently support,{" "} + + + contact us + {" "} + about adding an integration. +

+
+ ) : data.connection?.status === "connected" ? ( + +

+ Either the container image reference does not exist, or you + do not have access to it. +

+ {data?.provider?.id && ( + <> +

+ If you think you should have access, check your + integration configuration. +

+ + + View integration + + + )} +
+ ) : ( + +

+ This container image reference is from a supported registry, + but you haven't activated the integration yet. Activate + the integration to check if you have access to this image. +

+ + + Go to Integration + +
+ )} +
+ )} { @@ -80,6 +83,11 @@ export function BuildStatusBadge({ return isImageCompatibleWith(imageCheck, resourcePool.platform); }, [imageCheck, resourcePool]); + const privateImageNotFound = useMemo( + () => imageSourceCheck?.status === "invalid", + [imageSourceCheck?.status] + ); + const badgeIcon = buildStatus === "in_progress" ? ( @@ -90,6 +98,8 @@ export function BuildStatusBadge({ const badgeText = isCompatible === false ? "Image incompatible" + : privateImageNotFound + ? "Image not accessible" : buildStatus === "in_progress" ? "Build in progress" : buildStatus === "cancelled" @@ -99,7 +109,7 @@ export function BuildStatusBadge({ : "Build failed"; const badgeColorClasses = - isCompatible === false + isCompatible === false || privateImageNotFound ? ["border-danger", "bg-danger-subtle", "text-danger-emphasis"] : buildStatus === "in_progress" ? ["border-warning", "bg-warning-subtle", "text-warning-emphasis"] diff --git a/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx b/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx index 1fff1297a5..99817f3cf7 100644 --- a/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/BuilderEnvironmentFields.tsx @@ -19,10 +19,10 @@ import { skipToken } from "@reduxjs/toolkit/query"; import cx from "classnames"; import { useContext, useMemo } from "react"; -import { type Control } from "react-hook-form"; +import { useWatch, type Control, type Path } from "react-hook-form"; import { useProject } from "~/routes/projects/root"; -import { ErrorAlert, WarnAlert } from "../../../../components/Alert"; +import { ErrorAlert, InfoAlert, WarnAlert } from "../../../../components/Alert"; import RtkOrDataServicesError from "../../../../components/errors/RtkOrDataServicesError"; import { Loader } from "../../../../components/Loader"; import AppContext from "../../../../utils/context/appContext"; @@ -55,12 +55,26 @@ export default function BuilderEnvironmentFields({ repositories.length > 0 ? repositories : skipToken ); + const selectedRepositoryUrl = useWatch({ + control, + name: "repository" as Path, + }) as string; + + const selectedRepositoryIsPrivate = useMemo( + () => + data?.find( + (repo) => + repo.url === selectedRepositoryUrl && + repo.data?.metadata?.visibility === "private" + ), + [data, selectedRepositoryUrl] + ); + const firstEligibleRepository = useMemo( () => data?.findIndex( (repo) => - repo.data?.status === "valid" && - repo.data.metadata?.visibility === "public" + repo.data?.status === "valid" && repo.data.metadata?.pull_permission ), [data] ); @@ -91,8 +105,8 @@ export default function BuilderEnvironmentFields({ ) : firstEligibleRepository == null || firstEligibleRepository < 0 ? ( - No publicly accessible code repositories found in this project. RenkuLab - can only build session environments from public code repositories. + No accessible code repositories found in this project. Please ensure that + you have proper access to them. ) : (
@@ -102,6 +116,12 @@ export default function BuilderEnvironmentFields({ control={control} repositoriesDetails={data} /> + {selectedRepositoryIsPrivate && ( + + This is a private repository, launching the session will only be + available to users who have pull access to the repository. + + )}
@@ -114,9 +134,7 @@ export default function BuilderEnvironmentFields({
{!isEdit && (

- Let RenkuLab create a customized environment from a code repository. A - container image will be created based on the requirements found in the - code repository. + Let RenkuLab create a customized environment from a code repository.

)} {content} diff --git a/client/src/features/sessionsV2/components/SessionForm/CodeRepositorySelector.tsx b/client/src/features/sessionsV2/components/SessionForm/CodeRepositorySelector.tsx index b39062da3c..3c8b60e138 100644 --- a/client/src/features/sessionsV2/components/SessionForm/CodeRepositorySelector.tsx +++ b/client/src/features/sessionsV2/components/SessionForm/CodeRepositorySelector.tsx @@ -58,7 +58,7 @@ export default function CodeRepositorySelector({ : repositoriesDetails.find( (repo) => repo.data?.status === "valid" && - repo.data.metadata?.visibility === "public" + repo.data.metadata?.pull_permission )?.url, [controllerProps.defaultValue, repositoriesDetails] ); @@ -164,7 +164,7 @@ function CodeRepositorySelect({ unstyled isOptionDisabled={(option) => option.data?.status !== "valid" || - option.data.metadata?.visibility !== "public" + !option.data.metadata?.pull_permission } onChange={onChange} onBlur={onBlur}