@cobaltcore-dev/aurora 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/DeleteFlavorModal-C4nAi7gY.mjs +724 -0
- package/dist/client/DeleteFlavorModal-C4nAi7gY.mjs.map +1 -0
- package/dist/client/_flavorId-BPvssEzn.mjs +277 -0
- package/dist/client/_flavorId-BPvssEzn.mjs.map +1 -0
- package/dist/client/{_flavorId-CgueqKTs.mjs → _flavorId-CFUBcU-l.mjs} +3 -3
- package/dist/client/{_flavorId-CgueqKTs.mjs.map → _flavorId-CFUBcU-l.mjs.map} +1 -1
- package/dist/client/{_flavorId-B7fp6Nk8.mjs → _flavorId-CZcIp0Ih.mjs} +2 -2
- package/dist/client/{_flavorId-B7fp6Nk8.mjs.map → _flavorId-CZcIp0Ih.mjs.map} +1 -1
- package/dist/client/_imageId-BAezmV0Y.mjs +526 -0
- package/dist/client/_imageId-BAezmV0Y.mjs.map +1 -0
- package/dist/client/{_imageId-Ccv8PedW.mjs → _imageId-D-jqz3e3.mjs} +3 -3
- package/dist/client/_imageId-D-jqz3e3.mjs.map +1 -0
- package/dist/client/{_imageId-D9LR-02T.mjs → _imageId-gMdrdnVt.mjs} +2 -2
- package/dist/client/_imageId-gMdrdnVt.mjs.map +1 -0
- package/dist/client/{_projectId-BDARsteX.mjs → _projectId-BYccdAtX.mjs} +3 -3
- package/dist/client/{_projectId-BDARsteX.mjs.map → _projectId-BYccdAtX.mjs.map} +1 -1
- package/dist/client/{_projectId-ZBk6vka0.mjs → _projectId-CBjZC1tv.mjs} +5 -5
- package/dist/client/{_projectId-ZBk6vka0.mjs.map → _projectId-CBjZC1tv.mjs.map} +1 -1
- package/dist/client/{_projectId-CtyAHqhH.mjs → _projectId-CLEa1zgw.mjs} +2 -2
- package/dist/client/{_projectId-CtyAHqhH.mjs.map → _projectId-CLEa1zgw.mjs.map} +1 -1
- package/dist/client/{flavors-Ce3J-cFa.mjs → flavors-CJVSNz6R.mjs} +2 -2
- package/dist/client/{flavors-Ce3J-cFa.mjs.map → flavors-CJVSNz6R.mjs.map} +1 -1
- package/dist/client/{flavors-D5ThLaXO.mjs → flavors-CflJDIEH.mjs} +25 -24
- package/dist/client/{flavors-D5ThLaXO.mjs.map → flavors-CflJDIEH.mjs.map} +1 -1
- package/dist/client/imageHelpers-DBhL2ebL.mjs +1272 -0
- package/dist/client/imageHelpers-DBhL2ebL.mjs.map +1 -0
- package/dist/client/{images-C8M3ysWR.mjs → images-BsylWUiH.mjs} +836 -848
- package/dist/client/images-BsylWUiH.mjs.map +1 -0
- package/dist/client/{images-Cf5R28NR.mjs → images-zQq1nSVv.mjs} +2 -2
- package/dist/client/{images-Cf5R28NR.mjs.map → images-zQq1nSVv.mjs.map} +1 -1
- package/dist/client/index.js +10 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/{securitygroups-DXrfyuVF.mjs → securitygroups-CSmsBzUb.mjs} +104 -103
- package/dist/client/securitygroups-CSmsBzUb.mjs.map +1 -0
- package/dist/client/{securitygroups-67fFxiwt.mjs → securitygroups-T7kaNFEp.mjs} +2 -2
- package/dist/client/{securitygroups-67fFxiwt.mjs.map → securitygroups-T7kaNFEp.mjs.map} +1 -1
- package/dist/client/trpcClient.d.ts +0 -1
- package/dist/client/useErrorTranslation-Dc0eE8Zt.mjs +83 -0
- package/dist/client/useErrorTranslation-Dc0eE8Zt.mjs.map +1 -0
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +786 -534
- package/package.json +2 -2
- package/dist/client/DeleteFlavorModal-Be2GFiB_.mjs +0 -802
- package/dist/client/DeleteFlavorModal-Be2GFiB_.mjs.map +0 -1
- package/dist/client/ImageToastNotifications-BWfzOUMc.mjs +0 -1135
- package/dist/client/ImageToastNotifications-BWfzOUMc.mjs.map +0 -1
- package/dist/client/_flavorId-0PioWpEp.mjs +0 -276
- package/dist/client/_flavorId-0PioWpEp.mjs.map +0 -1
- package/dist/client/_imageId-Ccv8PedW.mjs.map +0 -1
- package/dist/client/_imageId-CiqEQB_J.mjs +0 -558
- package/dist/client/_imageId-CiqEQB_J.mjs.map +0 -1
- package/dist/client/_imageId-D9LR-02T.mjs.map +0 -1
- package/dist/client/images-C8M3ysWR.mjs.map +0 -1
- package/dist/client/securitygroups-DXrfyuVF.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_imageId-BAezmV0Y.mjs","names":["React","Container","Stack","Button","ButtonRow","Box","DescriptionList","DescriptionTerm","DescriptionDefinition","SizeDisplay","MEMBER_STATUSES","ClipboardText","TwoColumnDescriptionList","SharedImageBox","image","myMemberData","canUpdateMember","onStatusChange","isLoading","useLingui","isPending","status","PENDING","isRejected","REJECTED","sharedAt","created_at","Date","toLocaleString","t","updatedAt","updated_at","ownerProject","owner","p","className","span","ul","li","ACCEPTED","onClick","disabled","GeneralImageData","items","label","value","text","id","name","size","min_disk","min_ram","disk_format","container_format","toLocaleDateString","px","py","h2","SecuritySection","isSharedWithMe","visibility","protected","checksum","CustomPropertiesSection","knownFields","Set","customProperties","Object","entries","filter","key","has","sort","a","b","localeCompare","hasProperties","length","map","Fragment","undefined","JSON","stringify","String","ImageDetailsView","permissions","onMemberStatusChange","isMemberStatusChanging","actions","direction","gap","IMAGE_STATUSES","IMAGE_VISIBILITY","useImageActions","image","projectId","permissions","myMemberData","isShared","visibility","SHARED","isSharedWithMe","isOwnImage","owner","isPendingShared","status","isAcceptedShared","isDeactivated","DEACTIVATED","isPrivate","PRIVATE","isSharedVisibility","isProtected","protected","canViewDetails","canEditDetails","canUpdate","canEditMetadata","canDelete","canAccept","canUpdateMember","canReject","canManageAccess","canCreateMember","canDeleteMember","canSetToShared","canActivate","canDeactivate","hasAnyAction","Button","Stack","Status","PopupMenu","PopupMenuToggle","PopupMenuOptions","PopupMenuItem","PopupMenuSectionSeparator","toast","useNavigate","useParams","Trans","useLingui","trpcClient","trpcReact","useSetBreadcrumb","ImageDetailsView","EditImageDetailsModal","EditImageMetadataModal","DeleteImageModal","ActivateImageModal","DeactivateImageModal","ManageImageAccessModal","IMAGE_VISIBILITY","GlanceImage","MemberStatus","TRPCClientError","InferrableClientTypes","getImageAccessStatusUpdatedToast","getImageAccessStatusErrorToast","getImageUpdatedToast","getImageActivatedToast","getImageActivationErrorToast","getImageDeactivatedToast","getImageDeactivationErrorToast","getImageVisibilityUpdatedToast","getImageVisibilityUpdateErrorToast","useState","ContentHeader","useImageActions","RouteIdLevelDefaultError","convertToJsonPatchOperations","ImageErrorComponent","t","navigate","projectId","Route","to","params","RouteComponent","imageId","from","data","image","status","error","compute","getImageById","useQuery","project_id","id","name","permissionsData","canUser","permission","permissions","canDelete","canUpdate","canCreateMember","canDeleteMember","canUpdateMember","utils","useUtils","editDetailsModalOpen","setEditDetailsModalOpen","editMetadataModalOpen","setEditMetadataModalOpen","deleteModalOpen","setDeleteModalOpen","activateModalOpen","setActivateModalOpen","deactivateModalOpen","setDeactivateModalOpen","manageAccessModalOpen","setManageAccessModalOpen","updateImageMutation","updateImage","useMutation","onSuccess","updatedImage","setData","prev","listImagesWithPagination","invalidate","deleteImageMutation","deleteImage","deactivateImageMutation","deactivateImage","reactivateImageMutation","reactivateImage","updateImageVisibilityMutation","updateImageVisibility","isShared","visibility","SHARED","myMemberData","getImageMember","memberId","enabled","retry","updateMemberMutation","updateImageMember","listImageMembers","listSharedImagesByMemberStatus","handleMemberStatusChange","newStatus","mutateAsync","message","options","info","errorMessage","handleBack","isLoading","isPending","handleSaveEdit","updatedProperties","Partial","Promise","operations","String","success","handleDelete","deletedImage","handleActivate","img","Error","handleDeactivate","handleUpdateVisibility","newVisibility","actions","hasMoreActions","canEditMetadata","canSetToShared","canActivate","canDeactivate","canManageAccess","headerActions","canEditDetails","isSharedWithMe","protected","component"],"sources":["../../src/client/routes/_auth/projects/$projectId/compute/images/-components/ImageDetailsView.tsx","../../src/client/routes/_auth/projects/$projectId/compute/images/-hooks/useImageActions.ts","../../src/client/routes/_auth/projects/$projectId/compute/images/$imageId.tsx?tsr-split=component"],"sourcesContent":["import React from \"react\"\nimport {\n Container,\n Stack,\n Button,\n ButtonRow,\n Box,\n DescriptionList,\n DescriptionTerm,\n DescriptionDefinition,\n} from \"@cloudoperators/juno-ui-components\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { GlanceImage, ImageMember, MemberStatus } from \"@/server/Compute/types/image\"\nimport { SizeDisplay } from \"./SizeDisplay\"\nimport { MEMBER_STATUSES } from \"../../-constants/filters\"\nimport ClipboardText from \"@/client/components/ClipboardText\"\nimport { TwoColumnDescriptionList } from \"@/client/components/TwoColumnDescriptionList\"\n\ninterface ImageDetailsViewProps {\n image: GlanceImage\n isSharedWithMe?: boolean\n permissions?: {\n canCreateMember: boolean\n canDeleteMember: boolean\n canUpdateMember: boolean\n }\n myMemberData?: ImageMember\n onMemberStatusChange?: (status: MemberStatus) => void\n isMemberStatusChanging?: boolean\n actions?: React.ReactNode\n}\n\nconst SharedImageBox: React.FC<{\n image: GlanceImage\n myMemberData: ImageMember\n canUpdateMember: boolean\n onStatusChange: (status: MemberStatus) => void\n isLoading: boolean\n}> = ({ image, myMemberData, canUpdateMember, onStatusChange, isLoading }) => {\n const { t } = useLingui()\n const isPending = myMemberData.status === MEMBER_STATUSES.PENDING\n const isRejected = myMemberData.status === MEMBER_STATUSES.REJECTED\n\n const sharedAt = myMemberData.created_at ? new Date(myMemberData.created_at).toLocaleString() : t`N/A`\n const updatedAt = myMemberData.updated_at ? new Date(myMemberData.updated_at).toLocaleString() : t`N/A`\n const ownerProject = image.owner ?? \"\"\n\n return (\n <Box>\n {isPending && (\n <p className=\"text-theme-highest font-semibold\">\n <Trans>Your action is required</Trans>\n </p>\n )}\n <p>\n <Trans>\n This image was shared with you by <span className=\"font-semibold\">{ownerProject}</span> on {sharedAt}.\n </Trans>\n </p>\n <ul>\n <li>\n <span className=\"font-semibold\">\n <Trans>Access Status:</Trans>\n </span>{\" \"}\n {myMemberData.status}\n </li>\n <li>\n <span className=\"font-semibold\">\n <Trans>Shared:</Trans>\n </span>{\" \"}\n {sharedAt}\n </li>\n <li>\n <span className=\"font-semibold\">\n <Trans>Updated:</Trans>\n </span>{\" \"}\n {updatedAt}\n </li>\n </ul>\n\n {canUpdateMember && (isPending || isRejected || myMemberData.status === MEMBER_STATUSES.ACCEPTED) && (\n <ButtonRow>\n {(isPending || isRejected) && (\n <Button onClick={() => onStatusChange(MEMBER_STATUSES.ACCEPTED)} disabled={isLoading}>\n <Trans>Accept</Trans>\n </Button>\n )}\n {(isPending || myMemberData.status === MEMBER_STATUSES.ACCEPTED) && (\n <Button onClick={() => onStatusChange(MEMBER_STATUSES.REJECTED)} disabled={isLoading}>\n <Trans>Reject</Trans>\n </Button>\n )}\n </ButtonRow>\n )}\n </Box>\n )\n}\n\nexport const GeneralImageData: React.FC<{ image: GlanceImage }> = ({ image }) => {\n const { t } = useLingui()\n const items = [\n { label: t`ID`, value: <ClipboardText text={image.id} /> },\n { label: t`Name`, value: image.name },\n { label: t`Status`, value: image.status },\n { label: t`Size`, value: <SizeDisplay size={image.size} /> },\n { label: t`Min. Disk`, value: `${image.min_disk} GB` },\n { label: t`Min. RAM`, value: `${image.min_ram} MB` },\n { label: t`Disk Format`, value: <span className=\"uppercase\">{image.disk_format}</span> },\n { label: t`Container Format`, value: <span className=\"uppercase\">{image.container_format}</span> },\n {\n label: t`Created At`,\n value: image.created_at ? new Date(image.created_at).toLocaleDateString() : t`N/A`,\n },\n {\n label: t`Updated At`,\n value: image.updated_at ? new Date(image.updated_at).toLocaleDateString() : t`N/A`,\n },\n ]\n\n return (\n <Container px={false} py>\n <h2>{t`General Image Data`}</h2>\n <TwoColumnDescriptionList items={items} />\n </Container>\n )\n}\n\nexport const SecuritySection: React.FC<{ image: GlanceImage; isSharedWithMe?: boolean }> = ({\n image,\n isSharedWithMe = false,\n}) => {\n const { t } = useLingui()\n\n const items = [\n {\n label: isSharedWithMe ? t`Shared by Project` : t`Owner Project ID`,\n value: image.owner ? <ClipboardText text={image.owner} /> : \"\",\n },\n { label: t`Visibility`, value: image.visibility },\n { label: t`Protected`, value: image.protected ? t`Yes` : t`No` },\n { label: t`Checksum`, value: image?.checksum ? image.checksum : \"\" },\n ]\n\n return (\n <Container px={false} py>\n <h2>{t`Security`}</h2>\n <TwoColumnDescriptionList items={items} />\n </Container>\n )\n}\n\nexport const CustomPropertiesSection: React.FC<{ image: GlanceImage }> = ({ image }) => {\n const { t } = useLingui()\n\n const knownFields = new Set([\n \"id\",\n \"name\",\n \"status\",\n \"visibility\",\n \"size\",\n \"disk_format\",\n \"container_format\",\n \"min_disk\",\n \"min_ram\",\n \"owner\",\n \"protected\",\n \"created_at\",\n \"updated_at\",\n \"checksum\",\n ])\n\n const customProperties = Object.entries(image)\n .filter(([key]) => !knownFields.has(key))\n .sort(([a], [b]) => a.localeCompare(b))\n\n const hasProperties = customProperties.length > 0\n\n return (\n <Container px={false} py>\n <h2>{t`Metadata`}</h2>\n {hasProperties ? (\n <DescriptionList>\n {customProperties.map(([key, value]) => (\n <React.Fragment key={key}>\n <DescriptionTerm>{key}</DescriptionTerm>\n <DescriptionDefinition>\n {value === null || value === undefined ? (\n <span>null</span>\n ) : typeof value === \"object\" ? (\n <span className=\"break-all\">{JSON.stringify(value)}</span>\n ) : typeof value === \"boolean\" ? (\n value ? (\n t`True`\n ) : (\n t`False`\n )\n ) : (\n <span className=\"break-all\">{String(value)}</span>\n )}\n </DescriptionDefinition>\n </React.Fragment>\n ))}\n </DescriptionList>\n ) : (\n <p className=\"text-theme-light\">{t`No custom properties defined`}</p>\n )}\n </Container>\n )\n}\n\nexport const ImageDetailsView: React.FC<ImageDetailsViewProps> = ({\n image,\n isSharedWithMe = false,\n permissions,\n myMemberData,\n onMemberStatusChange,\n isMemberStatusChanging,\n actions,\n}) => {\n return (\n <Stack direction=\"vertical\" gap=\"6\">\n {isSharedWithMe && myMemberData && onMemberStatusChange && (\n <SharedImageBox\n image={image}\n myMemberData={myMemberData}\n canUpdateMember={permissions?.canUpdateMember ?? false}\n onStatusChange={onMemberStatusChange}\n isLoading={isMemberStatusChanging ?? false}\n />\n )}\n\n {actions}\n <GeneralImageData image={image} />\n <SecuritySection image={image} isSharedWithMe={isSharedWithMe} />\n <CustomPropertiesSection image={image} />\n </Stack>\n )\n}\n","import { GlanceImage, ImageMember } from \"@/server/Compute/types/image\"\nimport { IMAGE_STATUSES, IMAGE_VISIBILITY } from \"../../-constants/filters\"\n\nexport interface ImageActionsInput {\n image: GlanceImage\n projectId: string\n permissions: {\n canUpdate: boolean\n canDelete: boolean\n canCreateMember: boolean\n canDeleteMember: boolean\n canUpdateMember: boolean\n }\n myMemberData?: ImageMember | null\n}\n\nexport interface ImageActions {\n // Derived state\n isSharedWithMe: boolean\n isOwnImage: boolean\n isPendingShared: boolean\n isAcceptedShared: boolean\n isDeactivated: boolean\n isPrivate: boolean\n isSharedVisibility: boolean\n isProtected: boolean\n\n // Action flags\n canViewDetails: boolean\n canEditDetails: boolean\n canEditMetadata: boolean\n canDelete: boolean\n canAccept: boolean\n canReject: boolean\n canManageAccess: boolean\n canSetToShared: boolean\n canActivate: boolean\n canDeactivate: boolean\n\n // Convenience\n hasAnyAction: boolean\n}\n\n/**\n * Centralized hook for determining what actions are available for an image\n * based on ownership, permissions, and member status.\n *\n * Eliminates duplicate permission logic across ImageTableRow, $imageId, and ImageListView.\n */\nexport function useImageActions({ image, projectId, permissions, myMemberData }: ImageActionsInput): ImageActions {\n // Derived state\n const isShared = image.visibility === IMAGE_VISIBILITY.SHARED\n const isSharedWithMe = isShared && !!myMemberData\n const isOwnImage = image.owner === projectId\n const isPendingShared = isSharedWithMe && myMemberData?.status === \"pending\"\n const isAcceptedShared = isSharedWithMe && myMemberData?.status === \"accepted\"\n const isDeactivated = image.status === IMAGE_STATUSES.DEACTIVATED\n const isPrivate = image.visibility === IMAGE_VISIBILITY.PRIVATE\n const isSharedVisibility = image.visibility === IMAGE_VISIBILITY.SHARED\n const isProtected = !!image.protected\n\n // Action flags\n const canViewDetails = true // Always available if user can see the image\n const canEditDetails = isOwnImage && permissions.canUpdate\n const canEditMetadata = isOwnImage && permissions.canUpdate\n const canDelete = isOwnImage && permissions.canDelete && !isProtected\n const canAccept = isPendingShared && !isOwnImage && permissions.canUpdateMember\n const canReject = (isPendingShared || isAcceptedShared) && !isOwnImage && permissions.canUpdateMember\n const canManageAccess =\n isOwnImage && isSharedVisibility && (permissions.canCreateMember || permissions.canDeleteMember)\n const canSetToShared = isOwnImage && isPrivate && permissions.canUpdate\n const canActivate = isOwnImage && isDeactivated && permissions.canUpdate\n const canDeactivate = isOwnImage && !isDeactivated && permissions.canUpdate\n\n const hasAnyAction =\n canEditDetails ||\n canEditMetadata ||\n canDelete ||\n canAccept ||\n canReject ||\n canManageAccess ||\n canSetToShared ||\n canActivate ||\n canDeactivate\n\n return {\n // Derived state\n isSharedWithMe,\n isOwnImage,\n isPendingShared,\n isAcceptedShared,\n isDeactivated,\n isPrivate,\n isSharedVisibility,\n isProtected,\n\n // Action flags\n canViewDetails,\n canEditDetails,\n canEditMetadata,\n canDelete,\n canAccept,\n canReject,\n canManageAccess,\n canSetToShared,\n canActivate,\n canDeactivate,\n\n // Convenience\n hasAnyAction,\n }\n}\n","import {\n Button,\n Stack,\n Status,\n PopupMenu,\n PopupMenuToggle,\n PopupMenuOptions,\n PopupMenuItem,\n PopupMenuSectionSeparator,\n toast,\n} from \"@cloudoperators/juno-ui-components/index\"\nimport { createFileRoute, redirect, useNavigate, useParams } from \"@tanstack/react-router\"\nimport { z } from \"zod\"\nimport type { RouteInfo } from \"@/client/routes/routeInfo\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { getServiceIndex } from \"@/server/Authentication/helpers\"\nimport { trpcClient, trpcReact } from \"@/client/trpcClient\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\nimport { ImageDetailsView } from \"./-components/ImageDetailsView\"\nimport { EditImageDetailsModal } from \"./-components/EditImageDetailsModal\"\nimport { EditImageMetadataModal } from \"./-components/EditImageMetadataModal\"\nimport { DeleteImageModal } from \"./-components/DeleteImageModal\"\nimport { ActivateImageModal } from \"./-components/ActivateImageModal\"\nimport { DeactivateImageModal } from \"./-components/DeactivateImageModal\"\nimport { ManageImageAccessModal } from \"./-components/ManageImageAccessModal\"\nimport { IMAGE_VISIBILITY } from \"../-constants/filters\"\nimport { GlanceImage, MemberStatus } from \"@/server/Compute/types/image\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { InferrableClientTypes } from \"@trpc/server/unstable-core-do-not-import\"\nimport {\n getImageAccessStatusUpdatedToast,\n getImageAccessStatusErrorToast,\n getImageUpdatedToast,\n getImageActivatedToast,\n getImageActivationErrorToast,\n getImageDeactivatedToast,\n getImageDeactivationErrorToast,\n getImageVisibilityUpdatedToast,\n getImageVisibilityUpdateErrorToast,\n} from \"./-components/ImageToastNotifications\"\nimport { useState } from \"react\"\nimport { ContentHeader } from \"@/client/components/ContentHeader/ContentHeader\"\nimport { useImageActions } from \"./-hooks/useImageActions\"\nimport { RouteIdLevelDefaultError } from \"@/client/components/Errors/RouteIdLevelDefaultError\"\nimport { convertToJsonPatchOperations } from \"./-utils/imageHelpers\"\n\nfunction ImageErrorComponent() {\n const { t } = useLingui()\n const navigate = useNavigate()\n const { projectId } = Route.useParams()\n\n return (\n <RouteIdLevelDefaultError\n action={\n <Button\n variant=\"primary\"\n onClick={() => navigate({ to: \"/projects/$projectId/compute/images\", params: { projectId } })}\n >\n {t`Back to Images`}\n </Button>\n }\n />\n )\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId/compute/images/$imageId\")({\n staticData: {\n section: \"compute\",\n service: \"images\",\n analytics: {\n name: \"compute.images.detail\",\n },\n } satisfies RouteInfo,\n validateSearch: z.object({\n tab: z.enum([\"details\", \"sharing\"]).optional(),\n }),\n loader: async ({ context, params }) => {\n try {\n const image = await context.trpcClient?.compute.getImageById.query({\n project_id: params.projectId,\n imageId: params.imageId,\n })\n return { imageTitle: (image?.name as string | undefined) ?? image?.id ?? null }\n } catch {\n return { imageTitle: null }\n }\n },\n head: ({ loaderData }) => ({\n meta: [{ title: loaderData?.imageTitle ?? \"Image Details\" }],\n }),\n component: RouteComponent,\n beforeLoad: async ({ context, params }) => {\n const { trpcClient } = context\n const { projectId } = params\n\n const availableServices = (await trpcClient?.auth.getAvailableServices.query()) || []\n\n const serviceIndex = getServiceIndex(availableServices)\n\n // Redirect to the \"Projects Overview\" page if none of compute services available\n if (!serviceIndex[\"image\"] && !serviceIndex[\"compute\"]) {\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId },\n })\n }\n\n if (!serviceIndex[\"image\"][\"glance\"]) {\n // Redirect to the \"Compute Services Overview\" page if the \"Glance\" service is not available\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId: params.projectId },\n })\n }\n },\n errorComponent: ImageErrorComponent,\n})\n\nfunction RouteComponent() {\n const { projectId, imageId } = useParams({\n from: \"/_auth/projects/$projectId/compute/images/$imageId\",\n })\n\n const navigate = useNavigate()\n const { t } = useLingui()\n\n const {\n data: image,\n status,\n error,\n } = trpcReact.compute.getImageById.useQuery({ project_id: projectId, imageId: imageId })\n\n useSetBreadcrumb(Route.id, image?.name as string | undefined)\n\n const { data: permissionsData } = trpcReact.compute.canUser.useQuery({\n project_id: projectId,\n permission: [\n \"images:delete\",\n \"images:update\",\n \"images:create_member\",\n \"images:delete_member\",\n \"images:update_member\",\n ],\n })\n\n const permissions = {\n canDelete: permissionsData?.[0] ?? false,\n canUpdate: permissionsData?.[1] ?? false,\n canCreateMember: permissionsData?.[2] ?? false,\n canDeleteMember: permissionsData?.[3] ?? false,\n canUpdateMember: permissionsData?.[4] ?? false,\n }\n\n const utils = trpcReact.useUtils()\n\n const [editDetailsModalOpen, setEditDetailsModalOpen] = useState(false)\n const [editMetadataModalOpen, setEditMetadataModalOpen] = useState(false)\n const [deleteModalOpen, setDeleteModalOpen] = useState(false)\n const [activateModalOpen, setActivateModalOpen] = useState(false)\n const [deactivateModalOpen, setDeactivateModalOpen] = useState(false)\n const [manageAccessModalOpen, setManageAccessModalOpen] = useState(false)\n\n const updateImageMutation = trpcReact.compute.updateImage.useMutation({\n onSuccess: (updatedImage) => {\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deleteImageMutation = trpcReact.compute.deleteImage.useMutation({\n onSuccess: () => {\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deactivateImageMutation = trpcReact.compute.deactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const reactivateImageMutation = trpcReact.compute.reactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const updateImageVisibilityMutation = trpcReact.compute.updateImageVisibility.useMutation({\n onSuccess: (updatedImage) => {\n // Glance's PATCH response can omit fields such as `owner`. Merge into the\n // existing cached image so owner-gated UI (Edit Details, Manage Access)\n // isn't lost when only visibility changes.\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n },\n })\n\n // \"Shared with me\" = image is shared AND this project is a member. Owner\n // equality (image.owner === projectId) is unreliable, so derive from members.\n const isShared = image?.visibility === IMAGE_VISIBILITY.SHARED\n\n const { data: myMemberData } = trpcReact.compute.getImageMember.useQuery(\n { project_id: projectId, imageId: imageId, memberId: projectId },\n { enabled: isShared && !!imageId && !!projectId, retry: false }\n )\n\n const updateMemberMutation = trpcReact.compute.updateImageMember.useMutation({\n onSuccess: () => {\n utils.compute.getImageMember.invalidate({ project_id: projectId, imageId: imageId, memberId: projectId })\n utils.compute.listImageMembers.invalidate({ project_id: projectId, imageId: imageId })\n utils.compute.listImagesWithPagination.invalidate()\n utils.compute.listSharedImagesByMemberStatus.invalidate()\n },\n })\n\n const handleMemberStatusChange = async (newStatus: MemberStatus) => {\n try {\n await updateMemberMutation.mutateAsync({ project_id: projectId, imageId, memberId: projectId, status: newStatus })\n const { message, ...options } = getImageAccessStatusUpdatedToast(newStatus)\n toast.info(message, options)\n } catch (error) {\n const errorMessage = (error as TRPCClientError<InferrableClientTypes>)?.message\n const { message, ...options } = getImageAccessStatusErrorToast(errorMessage)\n toast.error(message, options)\n }\n }\n\n const handleBack = () => {\n navigate({\n to: \"/projects/$projectId/compute/images\",\n params: { projectId },\n })\n }\n\n const isLoading =\n updateImageMutation.isPending ||\n deleteImageMutation.isPending ||\n deactivateImageMutation.isPending ||\n reactivateImageMutation.isPending ||\n updateImageVisibilityMutation.isPending\n\n const handleSaveEdit = async (updatedProperties: Partial<GlanceImage>): Promise<boolean> => {\n if (!image) return false\n const operations = convertToJsonPatchOperations(updatedProperties, image)\n try {\n await updateImageMutation.mutateAsync({ project_id: projectId, imageId, operations })\n setEditDetailsModalOpen(false)\n const { message, ...options } = getImageUpdatedToast(String(image.name ?? image.id))\n toast.success(message, options)\n return true\n } catch {\n return false\n }\n }\n\n const handleDelete = async (deletedImage: GlanceImage) => {\n try {\n await deleteImageMutation.mutateAsync({ project_id: projectId, imageId: deletedImage.id })\n setDeleteModalOpen(false)\n handleBack()\n } catch {\n setDeleteModalOpen(false)\n }\n }\n\n const handleActivate = async (img: GlanceImage) => {\n try {\n await reactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleDeactivate = async (img: GlanceImage) => {\n try {\n await deactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleUpdateVisibility = async (newVisibility: \"public\" | \"private\" | \"shared\" | \"community\") => {\n if (!image) return\n try {\n await updateImageVisibilityMutation.mutateAsync({\n project_id: projectId,\n imageId: image.id,\n visibility: newVisibility,\n })\n const { message, ...options } = getImageVisibilityUpdatedToast(String(image.name ?? image.id), newVisibility)\n toast.success(message, options)\n } catch (error) {\n const { message, ...options } = getImageVisibilityUpdateErrorToast(\n String(image.name ?? image.id),\n (error as Error)?.message ?? \"\"\n )\n toast.error(message, options)\n }\n }\n\n // Handle loading state\n if (status === \"pending\") {\n return <Status status=\"progress\" title={t`Loading Image Details...`} />\n }\n\n // Handle error state\n if (status === \"error\") {\n const errorMessage = error?.message || t`Unknown Error`\n\n return (\n <RouteIdLevelDefaultError\n errorTitle={t`Error Loading Image`}\n errorDescription={errorMessage}\n action={\n <Button onClick={handleBack} variant=\"primary\">\n <Trans>Back to Images</Trans>\n </Button>\n }\n />\n )\n }\n\n // Handle no data state\n if (!image) {\n return <ImageErrorComponent />\n }\n\n // Use centralized actions hook\n const actions = useImageActions({\n image,\n projectId,\n permissions,\n myMemberData,\n })\n\n const hasMoreActions =\n actions.canEditMetadata ||\n actions.canSetToShared ||\n actions.canActivate ||\n actions.canDeactivate ||\n actions.canDelete ||\n actions.canManageAccess\n\n const headerActions = (hasMoreActions || actions.canEditDetails) && (\n <Stack gap=\"0.5\" alignment=\"center\">\n {hasMoreActions && (\n <PopupMenu className=\"flex items-center\">\n <PopupMenuToggle as=\"div\">\n <Button icon=\"moreVert\" title={t`More Actions`} disabled={isLoading} />\n </PopupMenuToggle>\n <PopupMenuOptions>\n {actions.canEditMetadata && (\n <PopupMenuItem\n onClick={() => setEditMetadataModalOpen(true)}\n label={t`Edit Metadata`}\n disabled={isLoading}\n />\n )}\n {(actions.canManageAccess || actions.canSetToShared || actions.canActivate || actions.canDeactivate) && (\n <PopupMenuSectionSeparator />\n )}\n {actions.canManageAccess && (\n <PopupMenuItem\n onClick={() => setManageAccessModalOpen(true)}\n label={t`Manage Access`}\n disabled={isLoading}\n />\n )}\n {actions.canSetToShared && (\n <PopupMenuItem label={t`Set to \"Shared\"`} onClick={() => handleUpdateVisibility(\"shared\")} />\n )}\n {actions.canActivate && (\n <PopupMenuItem label={t`Activate Image`} onClick={() => setActivateModalOpen(true)} />\n )}\n {actions.canDeactivate && (\n <PopupMenuItem label={t`Deactivate Image`} onClick={() => setDeactivateModalOpen(true)} />\n )}\n {actions.canDelete && (\n <>\n <PopupMenuSectionSeparator />\n <PopupMenuItem label={t`Delete Image`} onClick={() => setDeleteModalOpen(true)} />\n </>\n )}\n </PopupMenuOptions>\n </PopupMenu>\n )}\n\n {actions.canEditDetails && (\n <Button onClick={() => setEditDetailsModalOpen(true)} variant=\"primary\" disabled={isLoading}>\n <Trans>Edit Details</Trans>\n </Button>\n )}\n </Stack>\n )\n\n // Render success state\n return (\n <>\n <ContentHeader title={String(image.name ?? image.id)} projectId={projectId} actions={headerActions} />\n\n <div className=\"mt-3\">\n <ImageDetailsView\n key={image.id}\n image={image}\n isSharedWithMe={actions.isSharedWithMe}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n canUpdateMember: permissions.canUpdateMember,\n }}\n myMemberData={myMemberData}\n onMemberStatusChange={handleMemberStatusChange}\n isMemberStatusChanging={updateMemberMutation.isPending}\n />\n </div>\n\n {editDetailsModalOpen && (\n <EditImageDetailsModal\n image={image}\n isOpen={editDetailsModalOpen}\n isLoading={updateImageMutation.isPending}\n onClose={() => setEditDetailsModalOpen(false)}\n onSave={handleSaveEdit}\n />\n )}\n\n {editMetadataModalOpen && trpcClient && projectId && image && (\n <EditImageMetadataModal\n key=\"edit-metadata-modal\"\n client={trpcClient}\n image={image}\n isOpen={editMetadataModalOpen}\n onClose={() => {\n setEditMetadataModalOpen(false)\n // Invalidate queries only when modal closes to refresh with server data\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n utils.compute.listImagesWithPagination.invalidate()\n }}\n projectId={projectId}\n />\n )}\n\n {deleteModalOpen && (\n <DeleteImageModal\n image={image}\n isOpen={deleteModalOpen}\n isLoading={deleteImageMutation.isPending}\n isDisabled={!!image.protected}\n onClose={() => setDeleteModalOpen(false)}\n onDelete={handleDelete}\n />\n )}\n\n {activateModalOpen && (\n <ActivateImageModal\n image={image}\n isOpen={activateModalOpen}\n isLoading={reactivateImageMutation.isPending}\n onClose={() => setActivateModalOpen(false)}\n onActivate={handleActivate}\n />\n )}\n\n {deactivateModalOpen && (\n <DeactivateImageModal\n image={image}\n isOpen={deactivateModalOpen}\n isLoading={deactivateImageMutation.isPending}\n onClose={() => setDeactivateModalOpen(false)}\n onDeactivate={handleDeactivate}\n />\n )}\n\n {manageAccessModalOpen && (\n <ManageImageAccessModal\n image={image}\n isOpen={manageAccessModalOpen}\n onClose={() => setManageAccessModalOpen(false)}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n }}\n />\n )}\n </>\n )\n}\n"],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ var a = t("/_auth/projects/$projectId/compute/images/$imageId")({
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
head: ({ loaderData: e }) => ({ meta: [{ title: e?.imageTitle ?? "Image Details" }] }),
|
|
23
|
-
component: n(() => import("./_imageId-
|
|
23
|
+
component: n(() => import("./_imageId-BAezmV0Y.mjs"), "component"),
|
|
24
24
|
beforeLoad: async ({ context: t, params: n }) => {
|
|
25
25
|
let { trpcClient: i } = t, { projectId: a } = n, o = await i?.auth.getAvailableServices.query() || [], s = e(o);
|
|
26
26
|
if (!s.image && !s.compute) throw r({
|
|
@@ -32,9 +32,9 @@ var a = t("/_auth/projects/$projectId/compute/images/$imageId")({
|
|
|
32
32
|
params: { projectId: n.projectId }
|
|
33
33
|
});
|
|
34
34
|
},
|
|
35
|
-
errorComponent: n(() => import("./_imageId-
|
|
35
|
+
errorComponent: n(() => import("./_imageId-gMdrdnVt.mjs"), "errorComponent")
|
|
36
36
|
});
|
|
37
37
|
//#endregion
|
|
38
38
|
export { a as t };
|
|
39
39
|
|
|
40
|
-
//# sourceMappingURL=_imageId-
|
|
40
|
+
//# sourceMappingURL=_imageId-D-jqz3e3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_imageId-D-jqz3e3.mjs","names":["createFileRoute","redirect","z","getServiceIndex","GlanceImage","MemberStatus","Route","staticData","section","service","analytics","name","RouteInfo","validateSearch","object","tab","enum","optional","loader","context","params","image","trpcClient","compute","getImageById","query","project_id","projectId","imageId","imageTitle","id","head","loaderData","meta","title","component","lazyRouteComponent","$$splitComponentImporter","beforeLoad","availableServices","auth","getAvailableServices","serviceIndex","to","errorComponent","$$splitErrorComponentImporter"],"sources":["../../src/client/routes/_auth/projects/$projectId/compute/images/$imageId.tsx"],"sourcesContent":["import {\n Button,\n Stack,\n Status,\n PopupMenu,\n PopupMenuToggle,\n PopupMenuOptions,\n PopupMenuItem,\n PopupMenuSectionSeparator,\n toast,\n} from \"@cloudoperators/juno-ui-components/index\"\nimport { createFileRoute, redirect, useNavigate, useParams } from \"@tanstack/react-router\"\nimport { z } from \"zod\"\nimport type { RouteInfo } from \"@/client/routes/routeInfo\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { getServiceIndex } from \"@/server/Authentication/helpers\"\nimport { trpcClient, trpcReact } from \"@/client/trpcClient\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\nimport { ImageDetailsView } from \"./-components/ImageDetailsView\"\nimport { EditImageDetailsModal } from \"./-components/EditImageDetailsModal\"\nimport { EditImageMetadataModal } from \"./-components/EditImageMetadataModal\"\nimport { DeleteImageModal } from \"./-components/DeleteImageModal\"\nimport { ActivateImageModal } from \"./-components/ActivateImageModal\"\nimport { DeactivateImageModal } from \"./-components/DeactivateImageModal\"\nimport { ManageImageAccessModal } from \"./-components/ManageImageAccessModal\"\nimport { IMAGE_VISIBILITY } from \"../-constants/filters\"\nimport { GlanceImage, MemberStatus } from \"@/server/Compute/types/image\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { InferrableClientTypes } from \"@trpc/server/unstable-core-do-not-import\"\nimport {\n getImageAccessStatusUpdatedToast,\n getImageAccessStatusErrorToast,\n getImageUpdatedToast,\n getImageActivatedToast,\n getImageActivationErrorToast,\n getImageDeactivatedToast,\n getImageDeactivationErrorToast,\n getImageVisibilityUpdatedToast,\n getImageVisibilityUpdateErrorToast,\n} from \"./-components/ImageToastNotifications\"\nimport { useState } from \"react\"\nimport { ContentHeader } from \"@/client/components/ContentHeader/ContentHeader\"\nimport { useImageActions } from \"./-hooks/useImageActions\"\nimport { RouteIdLevelDefaultError } from \"@/client/components/Errors/RouteIdLevelDefaultError\"\nimport { convertToJsonPatchOperations } from \"./-utils/imageHelpers\"\n\nfunction ImageErrorComponent() {\n const { t } = useLingui()\n const navigate = useNavigate()\n const { projectId } = Route.useParams()\n\n return (\n <RouteIdLevelDefaultError\n action={\n <Button\n variant=\"primary\"\n onClick={() => navigate({ to: \"/projects/$projectId/compute/images\", params: { projectId } })}\n >\n {t`Back to Images`}\n </Button>\n }\n />\n )\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId/compute/images/$imageId\")({\n staticData: {\n section: \"compute\",\n service: \"images\",\n analytics: {\n name: \"compute.images.detail\",\n },\n } satisfies RouteInfo,\n validateSearch: z.object({\n tab: z.enum([\"details\", \"sharing\"]).optional(),\n }),\n loader: async ({ context, params }) => {\n try {\n const image = await context.trpcClient?.compute.getImageById.query({\n project_id: params.projectId,\n imageId: params.imageId,\n })\n return { imageTitle: (image?.name as string | undefined) ?? image?.id ?? null }\n } catch {\n return { imageTitle: null }\n }\n },\n head: ({ loaderData }) => ({\n meta: [{ title: loaderData?.imageTitle ?? \"Image Details\" }],\n }),\n component: RouteComponent,\n beforeLoad: async ({ context, params }) => {\n const { trpcClient } = context\n const { projectId } = params\n\n const availableServices = (await trpcClient?.auth.getAvailableServices.query()) || []\n\n const serviceIndex = getServiceIndex(availableServices)\n\n // Redirect to the \"Projects Overview\" page if none of compute services available\n if (!serviceIndex[\"image\"] && !serviceIndex[\"compute\"]) {\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId },\n })\n }\n\n if (!serviceIndex[\"image\"][\"glance\"]) {\n // Redirect to the \"Compute Services Overview\" page if the \"Glance\" service is not available\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId: params.projectId },\n })\n }\n },\n errorComponent: ImageErrorComponent,\n})\n\nfunction RouteComponent() {\n const { projectId, imageId } = useParams({\n from: \"/_auth/projects/$projectId/compute/images/$imageId\",\n })\n\n const navigate = useNavigate()\n const { t } = useLingui()\n\n const {\n data: image,\n status,\n error,\n } = trpcReact.compute.getImageById.useQuery({ project_id: projectId, imageId: imageId })\n\n useSetBreadcrumb(Route.id, image?.name as string | undefined)\n\n const { data: permissionsData } = trpcReact.compute.canUser.useQuery({\n project_id: projectId,\n permission: [\n \"images:delete\",\n \"images:update\",\n \"images:create_member\",\n \"images:delete_member\",\n \"images:update_member\",\n ],\n })\n\n const permissions = {\n canDelete: permissionsData?.[0] ?? false,\n canUpdate: permissionsData?.[1] ?? false,\n canCreateMember: permissionsData?.[2] ?? false,\n canDeleteMember: permissionsData?.[3] ?? false,\n canUpdateMember: permissionsData?.[4] ?? false,\n }\n\n const utils = trpcReact.useUtils()\n\n const [editDetailsModalOpen, setEditDetailsModalOpen] = useState(false)\n const [editMetadataModalOpen, setEditMetadataModalOpen] = useState(false)\n const [deleteModalOpen, setDeleteModalOpen] = useState(false)\n const [activateModalOpen, setActivateModalOpen] = useState(false)\n const [deactivateModalOpen, setDeactivateModalOpen] = useState(false)\n const [manageAccessModalOpen, setManageAccessModalOpen] = useState(false)\n\n const updateImageMutation = trpcReact.compute.updateImage.useMutation({\n onSuccess: (updatedImage) => {\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deleteImageMutation = trpcReact.compute.deleteImage.useMutation({\n onSuccess: () => {\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deactivateImageMutation = trpcReact.compute.deactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const reactivateImageMutation = trpcReact.compute.reactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const updateImageVisibilityMutation = trpcReact.compute.updateImageVisibility.useMutation({\n onSuccess: (updatedImage) => {\n // Glance's PATCH response can omit fields such as `owner`. Merge into the\n // existing cached image so owner-gated UI (Edit Details, Manage Access)\n // isn't lost when only visibility changes.\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n },\n })\n\n // \"Shared with me\" = image is shared AND this project is a member. Owner\n // equality (image.owner === projectId) is unreliable, so derive from members.\n const isShared = image?.visibility === IMAGE_VISIBILITY.SHARED\n\n const { data: myMemberData } = trpcReact.compute.getImageMember.useQuery(\n { project_id: projectId, imageId: imageId, memberId: projectId },\n { enabled: isShared && !!imageId && !!projectId, retry: false }\n )\n\n const updateMemberMutation = trpcReact.compute.updateImageMember.useMutation({\n onSuccess: () => {\n utils.compute.getImageMember.invalidate({ project_id: projectId, imageId: imageId, memberId: projectId })\n utils.compute.listImageMembers.invalidate({ project_id: projectId, imageId: imageId })\n utils.compute.listImagesWithPagination.invalidate()\n utils.compute.listSharedImagesByMemberStatus.invalidate()\n },\n })\n\n const handleMemberStatusChange = async (newStatus: MemberStatus) => {\n try {\n await updateMemberMutation.mutateAsync({ project_id: projectId, imageId, memberId: projectId, status: newStatus })\n const { message, ...options } = getImageAccessStatusUpdatedToast(newStatus)\n toast.info(message, options)\n } catch (error) {\n const errorMessage = (error as TRPCClientError<InferrableClientTypes>)?.message\n const { message, ...options } = getImageAccessStatusErrorToast(errorMessage)\n toast.error(message, options)\n }\n }\n\n const handleBack = () => {\n navigate({\n to: \"/projects/$projectId/compute/images\",\n params: { projectId },\n })\n }\n\n const isLoading =\n updateImageMutation.isPending ||\n deleteImageMutation.isPending ||\n deactivateImageMutation.isPending ||\n reactivateImageMutation.isPending ||\n updateImageVisibilityMutation.isPending\n\n const handleSaveEdit = async (updatedProperties: Partial<GlanceImage>): Promise<boolean> => {\n if (!image) return false\n const operations = convertToJsonPatchOperations(updatedProperties, image)\n try {\n await updateImageMutation.mutateAsync({ project_id: projectId, imageId, operations })\n setEditDetailsModalOpen(false)\n const { message, ...options } = getImageUpdatedToast(String(image.name ?? image.id))\n toast.success(message, options)\n return true\n } catch {\n return false\n }\n }\n\n const handleDelete = async (deletedImage: GlanceImage) => {\n try {\n await deleteImageMutation.mutateAsync({ project_id: projectId, imageId: deletedImage.id })\n setDeleteModalOpen(false)\n handleBack()\n } catch {\n setDeleteModalOpen(false)\n }\n }\n\n const handleActivate = async (img: GlanceImage) => {\n try {\n await reactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleDeactivate = async (img: GlanceImage) => {\n try {\n await deactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleUpdateVisibility = async (newVisibility: \"public\" | \"private\" | \"shared\" | \"community\") => {\n if (!image) return\n try {\n await updateImageVisibilityMutation.mutateAsync({\n project_id: projectId,\n imageId: image.id,\n visibility: newVisibility,\n })\n const { message, ...options } = getImageVisibilityUpdatedToast(String(image.name ?? image.id), newVisibility)\n toast.success(message, options)\n } catch (error) {\n const { message, ...options } = getImageVisibilityUpdateErrorToast(\n String(image.name ?? image.id),\n (error as Error)?.message ?? \"\"\n )\n toast.error(message, options)\n }\n }\n\n // Handle loading state\n if (status === \"pending\") {\n return <Status status=\"progress\" title={t`Loading Image Details...`} />\n }\n\n // Handle error state\n if (status === \"error\") {\n const errorMessage = error?.message || t`Unknown Error`\n\n return (\n <RouteIdLevelDefaultError\n errorTitle={t`Error Loading Image`}\n errorDescription={errorMessage}\n action={\n <Button onClick={handleBack} variant=\"primary\">\n <Trans>Back to Images</Trans>\n </Button>\n }\n />\n )\n }\n\n // Handle no data state\n if (!image) {\n return <ImageErrorComponent />\n }\n\n // Use centralized actions hook\n const actions = useImageActions({\n image,\n projectId,\n permissions,\n myMemberData,\n })\n\n const hasMoreActions =\n actions.canEditMetadata ||\n actions.canSetToShared ||\n actions.canActivate ||\n actions.canDeactivate ||\n actions.canDelete ||\n actions.canManageAccess\n\n const headerActions = (hasMoreActions || actions.canEditDetails) && (\n <Stack gap=\"0.5\" alignment=\"center\">\n {hasMoreActions && (\n <PopupMenu className=\"flex items-center\">\n <PopupMenuToggle as=\"div\">\n <Button icon=\"moreVert\" title={t`More Actions`} disabled={isLoading} />\n </PopupMenuToggle>\n <PopupMenuOptions>\n {actions.canEditMetadata && (\n <PopupMenuItem\n onClick={() => setEditMetadataModalOpen(true)}\n label={t`Edit Metadata`}\n disabled={isLoading}\n />\n )}\n {(actions.canManageAccess || actions.canSetToShared || actions.canActivate || actions.canDeactivate) && (\n <PopupMenuSectionSeparator />\n )}\n {actions.canManageAccess && (\n <PopupMenuItem\n onClick={() => setManageAccessModalOpen(true)}\n label={t`Manage Access`}\n disabled={isLoading}\n />\n )}\n {actions.canSetToShared && (\n <PopupMenuItem label={t`Set to \"Shared\"`} onClick={() => handleUpdateVisibility(\"shared\")} />\n )}\n {actions.canActivate && (\n <PopupMenuItem label={t`Activate Image`} onClick={() => setActivateModalOpen(true)} />\n )}\n {actions.canDeactivate && (\n <PopupMenuItem label={t`Deactivate Image`} onClick={() => setDeactivateModalOpen(true)} />\n )}\n {actions.canDelete && (\n <>\n <PopupMenuSectionSeparator />\n <PopupMenuItem label={t`Delete Image`} onClick={() => setDeleteModalOpen(true)} />\n </>\n )}\n </PopupMenuOptions>\n </PopupMenu>\n )}\n\n {actions.canEditDetails && (\n <Button onClick={() => setEditDetailsModalOpen(true)} variant=\"primary\" disabled={isLoading}>\n <Trans>Edit Details</Trans>\n </Button>\n )}\n </Stack>\n )\n\n // Render success state\n return (\n <>\n <ContentHeader title={String(image.name ?? image.id)} projectId={projectId} actions={headerActions} />\n\n <div className=\"mt-3\">\n <ImageDetailsView\n key={image.id}\n image={image}\n isSharedWithMe={actions.isSharedWithMe}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n canUpdateMember: permissions.canUpdateMember,\n }}\n myMemberData={myMemberData}\n onMemberStatusChange={handleMemberStatusChange}\n isMemberStatusChanging={updateMemberMutation.isPending}\n />\n </div>\n\n {editDetailsModalOpen && (\n <EditImageDetailsModal\n image={image}\n isOpen={editDetailsModalOpen}\n isLoading={updateImageMutation.isPending}\n onClose={() => setEditDetailsModalOpen(false)}\n onSave={handleSaveEdit}\n />\n )}\n\n {editMetadataModalOpen && trpcClient && projectId && image && (\n <EditImageMetadataModal\n key=\"edit-metadata-modal\"\n client={trpcClient}\n image={image}\n isOpen={editMetadataModalOpen}\n onClose={() => {\n setEditMetadataModalOpen(false)\n // Invalidate queries only when modal closes to refresh with server data\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n utils.compute.listImagesWithPagination.invalidate()\n }}\n projectId={projectId}\n />\n )}\n\n {deleteModalOpen && (\n <DeleteImageModal\n image={image}\n isOpen={deleteModalOpen}\n isLoading={deleteImageMutation.isPending}\n isDisabled={!!image.protected}\n onClose={() => setDeleteModalOpen(false)}\n onDelete={handleDelete}\n />\n )}\n\n {activateModalOpen && (\n <ActivateImageModal\n image={image}\n isOpen={activateModalOpen}\n isLoading={reactivateImageMutation.isPending}\n onClose={() => setActivateModalOpen(false)}\n onActivate={handleActivate}\n />\n )}\n\n {deactivateModalOpen && (\n <DeactivateImageModal\n image={image}\n isOpen={deactivateModalOpen}\n isLoading={deactivateImageMutation.isPending}\n onClose={() => setDeactivateModalOpen(false)}\n onDeactivate={handleDeactivate}\n />\n )}\n\n {manageAccessModalOpen && (\n <ManageImageAccessModal\n image={image}\n isOpen={manageAccessModalOpen}\n onClose={() => setManageAccessModalOpen(false)}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n }}\n />\n )}\n </>\n )\n}\n"],"mappings":";;;AAiEA,IAAaM,IAAQN,EAAgB,oDAAA,CAAA,CAAsD;CACzFO,YAAY;EACVC,SAAS;EACTC,SAAS;EACTC,WAAW,EACTC,MAAM,wBACR;CACF;CACAE,gBAAgBX,EAAEY,OAAO,EACvBC,KAAKb,EAAEc,KAAK,CAAC,WAAW,SAAU,CAAA,CAAA,CAAEC,SAAQ,EAC9C,CAAA;CACAC,QAAQ,OAAO,EAAEC,YAASC,gBAAQ;EAChC,IAAI;GACF,IAAMC,IAAQ,MAAMF,EAAQG,YAAYC,QAAQC,aAAaC,MAAM;IACjEC,YAAYN,EAAOO;IACnBC,SAASR,EAAOQ;GAClB,CAAA;GACA,OAAO,EAAEC,YAAaR,GAAOV,QAA+BU,GAAOS,MAAM,KAAK;EAChF,QAAQ;GACN,OAAO,EAAED,YAAY,KAAK;EAC5B;CACF;CACAE,OAAO,EAAEC,qBAAkB,EACzBC,MAAM,CAAC,EAAEC,OAAOF,GAAYH,cAAc,gBAAgB,CAAC,EAC7D;CACAM,WAASC,2CAAA,WAAA;CACTE,YAAY,OAAO,EAAEnB,YAASC,gBAAQ;EACpC,IAAM,EAAEE,kBAAeH,GACjB,EAAEQ,iBAAcP,GAEhBmB,IAAoB,MAAOjB,GAAYkB,KAAKC,qBAAqBhB,MAAAA,KAAY,CAAA,GAE7EiB,IAAevC,EAAgBoC,CAAAA;EAGrC,IAAI,CAACG,EAAa,SAAY,CAACA,EAAa,SAC1C,MAAMzC,EAAS;GACb0C,IAAI;GACJvB,QAAQ,EAAEO,aAAU;EACtB,CAAA;EAGF,IAAI,CAACe,EAAa,MAAS,QAEzB,MAAMzC,EAAS;GACb0C,IAAI;GACJvB,QAAQ,EAAEO,WAAWP,EAAOO,UAAU;EACxC,CAAA;CAEJ;CACAiB,gBAAcR,2CAAA,gBAAA;AAChB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as e } from "./build-WuxXRAqf.mjs";
|
|
2
|
-
import { t } from "./_imageId-
|
|
2
|
+
import { t } from "./_imageId-D-jqz3e3.mjs";
|
|
3
3
|
import { t as n } from "./RouteIdLevelDefaultError-DPF_x2tG.mjs";
|
|
4
4
|
import { jsx as r } from "react/jsx-runtime";
|
|
5
5
|
import { useNavigate as i } from "@tanstack/react-router";
|
|
@@ -19,4 +19,4 @@ function o() {
|
|
|
19
19
|
//#endregion
|
|
20
20
|
export { o as errorComponent };
|
|
21
21
|
|
|
22
|
-
//# sourceMappingURL=_imageId-
|
|
22
|
+
//# sourceMappingURL=_imageId-gMdrdnVt.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_imageId-gMdrdnVt.mjs","names":["Button","useNavigate","useLingui","GlanceImage","MemberStatus","RouteIdLevelDefaultError","ImageErrorComponent","t","navigate","projectId","Route","useParams","to","params","errorComponent"],"sources":["../../src/client/routes/_auth/projects/$projectId/compute/images/$imageId.tsx?tsr-split=errorComponent"],"sourcesContent":["import {\n Button,\n Stack,\n Status,\n PopupMenu,\n PopupMenuToggle,\n PopupMenuOptions,\n PopupMenuItem,\n PopupMenuSectionSeparator,\n toast,\n} from \"@cloudoperators/juno-ui-components/index\"\nimport { createFileRoute, redirect, useNavigate, useParams } from \"@tanstack/react-router\"\nimport { z } from \"zod\"\nimport type { RouteInfo } from \"@/client/routes/routeInfo\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { getServiceIndex } from \"@/server/Authentication/helpers\"\nimport { trpcClient, trpcReact } from \"@/client/trpcClient\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\nimport { ImageDetailsView } from \"./-components/ImageDetailsView\"\nimport { EditImageDetailsModal } from \"./-components/EditImageDetailsModal\"\nimport { EditImageMetadataModal } from \"./-components/EditImageMetadataModal\"\nimport { DeleteImageModal } from \"./-components/DeleteImageModal\"\nimport { ActivateImageModal } from \"./-components/ActivateImageModal\"\nimport { DeactivateImageModal } from \"./-components/DeactivateImageModal\"\nimport { ManageImageAccessModal } from \"./-components/ManageImageAccessModal\"\nimport { IMAGE_VISIBILITY } from \"../-constants/filters\"\nimport { GlanceImage, MemberStatus } from \"@/server/Compute/types/image\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { InferrableClientTypes } from \"@trpc/server/unstable-core-do-not-import\"\nimport {\n getImageAccessStatusUpdatedToast,\n getImageAccessStatusErrorToast,\n getImageUpdatedToast,\n getImageActivatedToast,\n getImageActivationErrorToast,\n getImageDeactivatedToast,\n getImageDeactivationErrorToast,\n getImageVisibilityUpdatedToast,\n getImageVisibilityUpdateErrorToast,\n} from \"./-components/ImageToastNotifications\"\nimport { useState } from \"react\"\nimport { ContentHeader } from \"@/client/components/ContentHeader/ContentHeader\"\nimport { useImageActions } from \"./-hooks/useImageActions\"\nimport { RouteIdLevelDefaultError } from \"@/client/components/Errors/RouteIdLevelDefaultError\"\nimport { convertToJsonPatchOperations } from \"./-utils/imageHelpers\"\n\nfunction ImageErrorComponent() {\n const { t } = useLingui()\n const navigate = useNavigate()\n const { projectId } = Route.useParams()\n\n return (\n <RouteIdLevelDefaultError\n action={\n <Button\n variant=\"primary\"\n onClick={() => navigate({ to: \"/projects/$projectId/compute/images\", params: { projectId } })}\n >\n {t`Back to Images`}\n </Button>\n }\n />\n )\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId/compute/images/$imageId\")({\n staticData: {\n section: \"compute\",\n service: \"images\",\n analytics: {\n name: \"compute.images.detail\",\n },\n } satisfies RouteInfo,\n validateSearch: z.object({\n tab: z.enum([\"details\", \"sharing\"]).optional(),\n }),\n loader: async ({ context, params }) => {\n try {\n const image = await context.trpcClient?.compute.getImageById.query({\n project_id: params.projectId,\n imageId: params.imageId,\n })\n return { imageTitle: (image?.name as string | undefined) ?? image?.id ?? null }\n } catch {\n return { imageTitle: null }\n }\n },\n head: ({ loaderData }) => ({\n meta: [{ title: loaderData?.imageTitle ?? \"Image Details\" }],\n }),\n component: RouteComponent,\n beforeLoad: async ({ context, params }) => {\n const { trpcClient } = context\n const { projectId } = params\n\n const availableServices = (await trpcClient?.auth.getAvailableServices.query()) || []\n\n const serviceIndex = getServiceIndex(availableServices)\n\n // Redirect to the \"Projects Overview\" page if none of compute services available\n if (!serviceIndex[\"image\"] && !serviceIndex[\"compute\"]) {\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId },\n })\n }\n\n if (!serviceIndex[\"image\"][\"glance\"]) {\n // Redirect to the \"Compute Services Overview\" page if the \"Glance\" service is not available\n throw redirect({\n to: \"/projects/$projectId\",\n params: { projectId: params.projectId },\n })\n }\n },\n errorComponent: ImageErrorComponent,\n})\n\nfunction RouteComponent() {\n const { projectId, imageId } = useParams({\n from: \"/_auth/projects/$projectId/compute/images/$imageId\",\n })\n\n const navigate = useNavigate()\n const { t } = useLingui()\n\n const {\n data: image,\n status,\n error,\n } = trpcReact.compute.getImageById.useQuery({ project_id: projectId, imageId: imageId })\n\n useSetBreadcrumb(Route.id, image?.name as string | undefined)\n\n const { data: permissionsData } = trpcReact.compute.canUser.useQuery({\n project_id: projectId,\n permission: [\n \"images:delete\",\n \"images:update\",\n \"images:create_member\",\n \"images:delete_member\",\n \"images:update_member\",\n ],\n })\n\n const permissions = {\n canDelete: permissionsData?.[0] ?? false,\n canUpdate: permissionsData?.[1] ?? false,\n canCreateMember: permissionsData?.[2] ?? false,\n canDeleteMember: permissionsData?.[3] ?? false,\n canUpdateMember: permissionsData?.[4] ?? false,\n }\n\n const utils = trpcReact.useUtils()\n\n const [editDetailsModalOpen, setEditDetailsModalOpen] = useState(false)\n const [editMetadataModalOpen, setEditMetadataModalOpen] = useState(false)\n const [deleteModalOpen, setDeleteModalOpen] = useState(false)\n const [activateModalOpen, setActivateModalOpen] = useState(false)\n const [deactivateModalOpen, setDeactivateModalOpen] = useState(false)\n const [manageAccessModalOpen, setManageAccessModalOpen] = useState(false)\n\n const updateImageMutation = trpcReact.compute.updateImage.useMutation({\n onSuccess: (updatedImage) => {\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deleteImageMutation = trpcReact.compute.deleteImage.useMutation({\n onSuccess: () => {\n utils.compute.listImagesWithPagination.invalidate()\n },\n })\n\n const deactivateImageMutation = trpcReact.compute.deactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const reactivateImageMutation = trpcReact.compute.reactivateImage.useMutation({\n onSuccess: () => {\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n },\n })\n\n const updateImageVisibilityMutation = trpcReact.compute.updateImageVisibility.useMutation({\n onSuccess: (updatedImage) => {\n // Glance's PATCH response can omit fields such as `owner`. Merge into the\n // existing cached image so owner-gated UI (Edit Details, Manage Access)\n // isn't lost when only visibility changes.\n utils.compute.getImageById.setData({ project_id: projectId, imageId }, (prev) =>\n prev ? { ...prev, ...updatedImage } : updatedImage\n )\n },\n })\n\n // \"Shared with me\" = image is shared AND this project is a member. Owner\n // equality (image.owner === projectId) is unreliable, so derive from members.\n const isShared = image?.visibility === IMAGE_VISIBILITY.SHARED\n\n const { data: myMemberData } = trpcReact.compute.getImageMember.useQuery(\n { project_id: projectId, imageId: imageId, memberId: projectId },\n { enabled: isShared && !!imageId && !!projectId, retry: false }\n )\n\n const updateMemberMutation = trpcReact.compute.updateImageMember.useMutation({\n onSuccess: () => {\n utils.compute.getImageMember.invalidate({ project_id: projectId, imageId: imageId, memberId: projectId })\n utils.compute.listImageMembers.invalidate({ project_id: projectId, imageId: imageId })\n utils.compute.listImagesWithPagination.invalidate()\n utils.compute.listSharedImagesByMemberStatus.invalidate()\n },\n })\n\n const handleMemberStatusChange = async (newStatus: MemberStatus) => {\n try {\n await updateMemberMutation.mutateAsync({ project_id: projectId, imageId, memberId: projectId, status: newStatus })\n const { message, ...options } = getImageAccessStatusUpdatedToast(newStatus)\n toast.info(message, options)\n } catch (error) {\n const errorMessage = (error as TRPCClientError<InferrableClientTypes>)?.message\n const { message, ...options } = getImageAccessStatusErrorToast(errorMessage)\n toast.error(message, options)\n }\n }\n\n const handleBack = () => {\n navigate({\n to: \"/projects/$projectId/compute/images\",\n params: { projectId },\n })\n }\n\n const isLoading =\n updateImageMutation.isPending ||\n deleteImageMutation.isPending ||\n deactivateImageMutation.isPending ||\n reactivateImageMutation.isPending ||\n updateImageVisibilityMutation.isPending\n\n const handleSaveEdit = async (updatedProperties: Partial<GlanceImage>): Promise<boolean> => {\n if (!image) return false\n const operations = convertToJsonPatchOperations(updatedProperties, image)\n try {\n await updateImageMutation.mutateAsync({ project_id: projectId, imageId, operations })\n setEditDetailsModalOpen(false)\n const { message, ...options } = getImageUpdatedToast(String(image.name ?? image.id))\n toast.success(message, options)\n return true\n } catch {\n return false\n }\n }\n\n const handleDelete = async (deletedImage: GlanceImage) => {\n try {\n await deleteImageMutation.mutateAsync({ project_id: projectId, imageId: deletedImage.id })\n setDeleteModalOpen(false)\n handleBack()\n } catch {\n setDeleteModalOpen(false)\n }\n }\n\n const handleActivate = async (img: GlanceImage) => {\n try {\n await reactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setActivateModalOpen(false)\n const { message, ...options } = getImageActivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleDeactivate = async (img: GlanceImage) => {\n try {\n await deactivateImageMutation.mutateAsync({ project_id: projectId, imageId: img.id })\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivatedToast(String(img.name ?? img.id))\n toast.success(message, options)\n } catch (error) {\n setDeactivateModalOpen(false)\n const { message, ...options } = getImageDeactivationErrorToast(img.id, (error as Error)?.message ?? \"\")\n toast.error(message, options)\n }\n }\n\n const handleUpdateVisibility = async (newVisibility: \"public\" | \"private\" | \"shared\" | \"community\") => {\n if (!image) return\n try {\n await updateImageVisibilityMutation.mutateAsync({\n project_id: projectId,\n imageId: image.id,\n visibility: newVisibility,\n })\n const { message, ...options } = getImageVisibilityUpdatedToast(String(image.name ?? image.id), newVisibility)\n toast.success(message, options)\n } catch (error) {\n const { message, ...options } = getImageVisibilityUpdateErrorToast(\n String(image.name ?? image.id),\n (error as Error)?.message ?? \"\"\n )\n toast.error(message, options)\n }\n }\n\n // Handle loading state\n if (status === \"pending\") {\n return <Status status=\"progress\" title={t`Loading Image Details...`} />\n }\n\n // Handle error state\n if (status === \"error\") {\n const errorMessage = error?.message || t`Unknown Error`\n\n return (\n <RouteIdLevelDefaultError\n errorTitle={t`Error Loading Image`}\n errorDescription={errorMessage}\n action={\n <Button onClick={handleBack} variant=\"primary\">\n <Trans>Back to Images</Trans>\n </Button>\n }\n />\n )\n }\n\n // Handle no data state\n if (!image) {\n return <ImageErrorComponent />\n }\n\n // Use centralized actions hook\n const actions = useImageActions({\n image,\n projectId,\n permissions,\n myMemberData,\n })\n\n const hasMoreActions =\n actions.canEditMetadata ||\n actions.canSetToShared ||\n actions.canActivate ||\n actions.canDeactivate ||\n actions.canDelete ||\n actions.canManageAccess\n\n const headerActions = (hasMoreActions || actions.canEditDetails) && (\n <Stack gap=\"0.5\" alignment=\"center\">\n {hasMoreActions && (\n <PopupMenu className=\"flex items-center\">\n <PopupMenuToggle as=\"div\">\n <Button icon=\"moreVert\" title={t`More Actions`} disabled={isLoading} />\n </PopupMenuToggle>\n <PopupMenuOptions>\n {actions.canEditMetadata && (\n <PopupMenuItem\n onClick={() => setEditMetadataModalOpen(true)}\n label={t`Edit Metadata`}\n disabled={isLoading}\n />\n )}\n {(actions.canManageAccess || actions.canSetToShared || actions.canActivate || actions.canDeactivate) && (\n <PopupMenuSectionSeparator />\n )}\n {actions.canManageAccess && (\n <PopupMenuItem\n onClick={() => setManageAccessModalOpen(true)}\n label={t`Manage Access`}\n disabled={isLoading}\n />\n )}\n {actions.canSetToShared && (\n <PopupMenuItem label={t`Set to \"Shared\"`} onClick={() => handleUpdateVisibility(\"shared\")} />\n )}\n {actions.canActivate && (\n <PopupMenuItem label={t`Activate Image`} onClick={() => setActivateModalOpen(true)} />\n )}\n {actions.canDeactivate && (\n <PopupMenuItem label={t`Deactivate Image`} onClick={() => setDeactivateModalOpen(true)} />\n )}\n {actions.canDelete && (\n <>\n <PopupMenuSectionSeparator />\n <PopupMenuItem label={t`Delete Image`} onClick={() => setDeleteModalOpen(true)} />\n </>\n )}\n </PopupMenuOptions>\n </PopupMenu>\n )}\n\n {actions.canEditDetails && (\n <Button onClick={() => setEditDetailsModalOpen(true)} variant=\"primary\" disabled={isLoading}>\n <Trans>Edit Details</Trans>\n </Button>\n )}\n </Stack>\n )\n\n // Render success state\n return (\n <>\n <ContentHeader title={String(image.name ?? image.id)} projectId={projectId} actions={headerActions} />\n\n <div className=\"mt-3\">\n <ImageDetailsView\n key={image.id}\n image={image}\n isSharedWithMe={actions.isSharedWithMe}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n canUpdateMember: permissions.canUpdateMember,\n }}\n myMemberData={myMemberData}\n onMemberStatusChange={handleMemberStatusChange}\n isMemberStatusChanging={updateMemberMutation.isPending}\n />\n </div>\n\n {editDetailsModalOpen && (\n <EditImageDetailsModal\n image={image}\n isOpen={editDetailsModalOpen}\n isLoading={updateImageMutation.isPending}\n onClose={() => setEditDetailsModalOpen(false)}\n onSave={handleSaveEdit}\n />\n )}\n\n {editMetadataModalOpen && trpcClient && projectId && image && (\n <EditImageMetadataModal\n key=\"edit-metadata-modal\"\n client={trpcClient}\n image={image}\n isOpen={editMetadataModalOpen}\n onClose={() => {\n setEditMetadataModalOpen(false)\n // Invalidate queries only when modal closes to refresh with server data\n utils.compute.getImageById.invalidate({ project_id: projectId, imageId })\n utils.compute.listImagesWithPagination.invalidate()\n }}\n projectId={projectId}\n />\n )}\n\n {deleteModalOpen && (\n <DeleteImageModal\n image={image}\n isOpen={deleteModalOpen}\n isLoading={deleteImageMutation.isPending}\n isDisabled={!!image.protected}\n onClose={() => setDeleteModalOpen(false)}\n onDelete={handleDelete}\n />\n )}\n\n {activateModalOpen && (\n <ActivateImageModal\n image={image}\n isOpen={activateModalOpen}\n isLoading={reactivateImageMutation.isPending}\n onClose={() => setActivateModalOpen(false)}\n onActivate={handleActivate}\n />\n )}\n\n {deactivateModalOpen && (\n <DeactivateImageModal\n image={image}\n isOpen={deactivateModalOpen}\n isLoading={deactivateImageMutation.isPending}\n onClose={() => setDeactivateModalOpen(false)}\n onDeactivate={handleDeactivate}\n />\n )}\n\n {manageAccessModalOpen && (\n <ManageImageAccessModal\n image={image}\n isOpen={manageAccessModalOpen}\n onClose={() => setManageAccessModalOpen(false)}\n permissions={{\n canCreateMember: permissions.canCreateMember,\n canDeleteMember: permissions.canDeleteMember,\n }}\n />\n )}\n </>\n )\n}\n"],"mappings":";;;;;;;AA8CA,SAASM,IAAAA;CACP,IAAM,EAAA,MAAA,GAAA,GAAA,MAAQJ,EAAAA,GACRM,IAAWP,EAAAA,GACX,EAAEQ,iBAAcC,EAAMC,UAAS;CAErC,OACE,gBAAC,GAAA,EACC,QACE,gBAAC,GAAA;EACC,SAAQ;EACR,eAAeH,EAAS;GAAEI,IAAI;GAAuCC,QAAQ,EAAEJ,aAAU;EAAE,CAAA;EAE1FF,UAAAA,EAAAA,EAAC,EAAA,IAAA,SAAA,CAAA;;AAKZ"}
|
|
@@ -18,8 +18,8 @@ function l({ error: r, title: i, helpText: o, safeErrorMessage: l }) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
var u = r("/_auth/projects/$projectId")({
|
|
21
|
-
component: i(() => import("./_projectId-
|
|
22
|
-
errorComponent: i(() => import("./_projectId-
|
|
21
|
+
component: i(() => import("./_projectId-CBjZC1tv.mjs"), "component"),
|
|
22
|
+
errorComponent: i(() => import("./_projectId-CLEa1zgw.mjs"), "errorComponent"),
|
|
23
23
|
loader: async (e) => {
|
|
24
24
|
let { context: t, params: n } = e, r, i;
|
|
25
25
|
try {
|
|
@@ -74,4 +74,4 @@ var u = r("/_auth/projects/$projectId")({
|
|
|
74
74
|
//#endregion
|
|
75
75
|
export { l as n, u as t };
|
|
76
76
|
|
|
77
|
-
//# sourceMappingURL=_projectId-
|
|
77
|
+
//# sourceMappingURL=_projectId-BYccdAtX.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_projectId-BDARsteX.mjs","names":["Status","Button","useNavigate","RouteError","error","title","helpText","safeErrorMessage","useLingui","navigate","defaultTitle","t","defaultHelpText","defaultErrorMessage","errorMessage","safeMessage","additionalInfo","status","body","action","variant","onClick","to","createFileRoute","TRPCClientError","ScopeError","type","code","message","currentDomain","id","name","Route","component","lazyRouteComponent","$$splitComponentImporter","errorComponent","$$splitErrorComponentImporter","loader","options","context","params","scopeData","scopeError","trpcClient","auth","setCurrentScope","mutate","projectId","error","currentScope","getCurrentScope","query","domain","data","crumbDomain","undefined","crumbProject","availableServices","accountId","description","project","Promise","all","getAvailableServices","getProject","catch","path"],"sources":["../../src/client/components/Errors/RouteError.tsx","../../src/client/routes/_auth/projects/$projectId.tsx"],"sourcesContent":["import { Status, Button } from \"@cloudoperators/juno-ui-components\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { useNavigate } from \"@tanstack/react-router\"\n\ninterface RouteErrorProps {\n error: unknown\n title?: string\n helpText?: string\n /** Explicitly pass a safe error message to display. Use this when you want to show specific error details. */\n safeErrorMessage?: string\n}\n\n/**\n * Renders errors that occur outside the application layout, such as an expired\n * cookie or token. In these cases, the normal page content is unavailable and\n * only the header may remain visible.\n */\nexport function RouteError({ error, title, helpText, safeErrorMessage }: RouteErrorProps) {\n const { t } = useLingui()\n const navigate = useNavigate()\n\n const defaultTitle = t`Unable to Load Content`\n const defaultHelpText = t`This could be due to insufficient permissions or a temporary service issue. Please check your access rights or try refreshing the page.`\n const defaultErrorMessage = t`An unexpected error occurred.`\n\n // Security: Do not expose raw Error.message by default as it may contain sensitive information.\n // Only display:\n // 1. Explicitly passed safeErrorMessage\n // 2. String errors (when caller explicitly passes a safe string)\n // 3. error.safeMessage if present (for controlled error exposure)\n // 4. Default translated message for all other cases\n const errorMessage =\n safeErrorMessage ||\n (typeof error === \"string\" ? error : null) ||\n (error && typeof error === \"object\" && \"safeMessage\" in error && typeof error.safeMessage === \"string\"\n ? error.safeMessage\n : null) ||\n defaultErrorMessage\n\n const additionalInfo = helpText || defaultHelpText\n\n return (\n <Status\n status=\"error\"\n title={title || defaultTitle}\n body={errorMessage + \" \" + additionalInfo}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Go to Home</Trans>\n </Button>\n }\n />\n )\n}\n","import { createFileRoute, Outlet, useLoaderData, useRouteContext } from \"@tanstack/react-router\"\nimport { AppShell, Button, Container, Stack, Status } from \"@cloudoperators/juno-ui-components\"\nimport { SideNavBar } from \"@/client/routes/_auth/projects/-components/SideNavBar\"\nimport { buildNavSections } from \"@/client/routes/_auth/projects/-components/buildNavSections\"\nimport { Breadcrumbs } from \"@/client/components/Breadcrumbs\"\nimport { RouteError } from \"@/client/components/Errors/RouteError\"\nimport { useMemo } from \"react\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { BreadcrumbExtensionProvider } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\n\n// Type for scope error that we catch and handle gracefully\ntype ScopeError = {\n type: \"scope_error\"\n code: string\n message: string\n currentDomain?: { id: string; name: string } | null\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId\")({\n component: RouteComponent,\n errorComponent: ProjectErrorComponent,\n loader: async (options) => {\n const { context, params } = options\n\n // Catch rescope errors and return them as data instead of throwing\n // This allows the component to render a friendly error UI\n let scopeData\n let scopeError: ScopeError | undefined\n\n try {\n scopeData = await context.trpcClient?.auth.setCurrentScope.mutate({\n type: \"project\",\n projectId: params.projectId || \"\",\n })\n } catch (error) {\n if (error instanceof TRPCClientError) {\n // Try to get the current session's domain to show in the error message\n let currentDomain: { id: string; name: string } | null | undefined\n try {\n const currentScope = await context.trpcClient?.auth.getCurrentScope.query()\n if (currentScope?.domain?.id && currentScope?.domain?.name) {\n currentDomain = { id: currentScope.domain.id, name: currentScope.domain.name }\n }\n } catch {\n // Ignore errors fetching current scope\n }\n\n // Return error as data so the component can handle it\n scopeError = {\n type: \"scope_error\",\n code: error.data?.code || \"UNKNOWN\",\n message: error.message,\n currentDomain,\n }\n } else {\n // Unknown errors should still propagate\n throw error\n }\n }\n\n // If scope failed, return early with error state\n if (scopeError) {\n return {\n scopeError,\n projectId: params.projectId,\n // Provide minimal data for error rendering\n trpcClient: context.trpcClient,\n crumbDomain: undefined,\n crumbProject: undefined,\n availableServices: undefined,\n accountId: \"\",\n description: null,\n }\n }\n\n const [availableServices, project] = await Promise.all([\n context.trpcClient?.auth.getAvailableServices.query(),\n context.trpcClient?.project.getProject.query({ projectId: params.projectId }).catch(() => null),\n ])\n\n const accountId = scopeData?.domain?.id || \"\"\n const description = project?.description ?? null\n\n return {\n scopeError: undefined,\n trpcClient: context.trpcClient,\n crumbDomain: { path: `/projects`, name: scopeData?.domain?.name },\n crumbProject: scopeData?.project,\n availableServices,\n accountId,\n projectId: params.projectId,\n description,\n }\n },\n})\n\nfunction RouteComponent() {\n const { t } = useLingui()\n const loaderData = useLoaderData({ from: Route.id })\n const navigate = Route.useNavigate()\n const { enabledServices, serviceExtensions } = useRouteContext({ strict: false })\n\n const projectName = loaderData.crumbProject?.name || loaderData.projectId\n const projectLabel = loaderData.crumbDomain?.name ? `${loaderData.crumbDomain.name}/${projectName}` : projectName\n useSetBreadcrumb(Route.id, projectLabel)\n\n // Handle scope errors with a friendly UI using Juno's Status component\n if (loaderData.scopeError) {\n const { code, message, currentDomain } = loaderData.scopeError\n const domainName = currentDomain?.name\n\n if (code === \"UNAUTHORIZED\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n title={t`Session Changed`}\n body={t`Your session was changed or ended in another browser tab. This can happen if you logged out or switched to a different account or domain elsewhere. Please log in again to continue.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Sign in</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"NOT_FOUND\") {\n const bodyText = domainName\n ? t`This project doesn't exist or is not accessible from your current domain. Your current domain is ${domainName}. Please select a project from your current domain.`\n : t`This project doesn't exist or is not accessible from your current domain. Please select a project from your current domain.`\n\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Accessible`}\n body={bodyText}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"FORBIDDEN\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={403}\n title={t`Access Denied`}\n body={t`You don't have permission to access this project.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // Fallback for other error codes\n return <RouteError error={new Error(message)} />\n }\n\n const { availableServices, projectId, crumbProject, crumbDomain } = loaderData\n\n const sections = useMemo(\n () => buildNavSections(projectId, availableServices!, enabledServices, serviceExtensions),\n [projectId, availableServices, enabledServices, serviceExtensions]\n )\n\n return (\n <AppShell\n embedded\n sideNavigation={\n <SideNavBar\n sections={sections}\n projectId={projectId}\n projectName={crumbProject?.name || projectId}\n domainName={crumbDomain?.name}\n />\n }\n className=\"h-min-screen\"\n >\n <Container>\n <Stack direction=\"vertical\" distribution=\"start\" alignment=\"stretch\" className=\"xl:flex-row\" gap=\"6\">\n {/* Main content area */}\n <div className=\"min-w-0 flex-1\">\n <BreadcrumbExtensionProvider>\n <Breadcrumbs />\n <Outlet />\n </BreadcrumbExtensionProvider>\n </div>\n </Stack>\n </Container>\n </AppShell>\n )\n}\n\nfunction ProjectErrorComponent({ error }: { error: Error & { statusCode?: number } }) {\n const { t } = useLingui()\n const navigate = Route.useNavigate()\n\n // Handle 404 errors with a custom UI\n if (error.statusCode === 404 || error.message === \"Project not found\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Found`}\n body={t`The project you are looking for doesn't exist or you don't have access to it.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // For all other errors, use the default RouteError component\n return <RouteError error={error} safeErrorMessage={error instanceof TRPCClientError ? error.message : undefined} />\n}\n"],"mappings":";;;;;;AAiBA,SAAgBG,EAAW,EAAEC,UAAOC,UAAOC,aAAUC,uBAAmC;CACtF,IAAM,EAAA,MAAA,GAAA,GAAA,MAAQC,EAAAA,GACRC,IAAWP,EAAAA,GAEXQ,IAAeC,EAAAA,EAAC,EAAA,IAAA,SAAuB,CAAA,GACvCC,IAAkBD,EAAAA,EAAC,EAAA,IAAA,SAAwI,CAAA,GAC3JE,IAAsBF,EAAAA,EAAC,EAAA,IAAA,SAA8B,CAAA,GAQrDG,IACJP,MACC,OAAOH,KAAU,WAAWA,IAAQ,UACpCA,KAAS,OAAOA,KAAU,YAAY,iBAAiBA,KAAS,OAAOA,EAAMW,eAAgB,WAC1FX,EAAMW,cACN,SACJF,GAEIG,IAAiBV,KAAYM;CAEnC,OACE,gBAACZ,GAAAA;EACCiB,QAAO;EACPZ,OAAOA,KAASK;EAChBQ,MAAMJ,IAAe,MAAME;EAC3BG,QACE,gBAAClB,GAAAA;GAAOmB,SAAQ;GAAUC,eAAeZ,EAAS,EAAEa,IAAI,IAAI,CAAA;GAC1D,UAAA,gBAAA,GAAA,EAAA,IAAA,SAAA,CAAA;;;AAKV;ACjCA,IAAaU,IAAQT,EAAgB,4BAAA,CAAA,CAA8B;CACjEU,WAASC,6CAAA,WAAA;CACTE,gBAAcF,6CAAA,gBAAA;CACdI,QAAQ,OAAOC,MAAAA;EACb,IAAM,EAAEC,YAASC,cAAWF,GAIxBG,GACAC;EAEJ,IAAI;GACFD,IAAY,MAAMF,EAAQI,YAAYC,KAAKC,gBAAgBC,OAAO;IAChErB,MAAM;IACNsB,WAAWP,EAAOO,aAAa;GACjC,CAAA;EACF,SAASC,GAAO;GACd,IAAIA,aAAiBzB,GAAiB;IAEpC,IAAIK;IACJ,IAAI;KACF,IAAMqB,IAAe,MAAMV,EAAQI,YAAYC,KAAKM,gBAAgBC,MAAAA;KACpE,AAAIF,GAAcG,QAAQvB,MAAMoB,GAAcG,QAAQtB,SACpDF,IAAgB;MAAEC,IAAIoB,EAAaG,OAAOvB;MAAIC,MAAMmB,EAAaG,OAAOtB;KAAK;IAEjF,QAAQ,CACN;IAIFY,IAAa;KACXjB,MAAM;KACNC,MAAMsB,EAAMK,MAAM3B,QAAQ;KAC1BC,SAASqB,EAAMrB;KACfC;IACF;GACF,OAEE,MAAMoB;EAEV;EAGA,IAAIN,GACF,OAAO;GACLA;GACAK,WAAWP,EAAOO;GAElBJ,YAAYJ,EAAQI;GACpBW,aAAaC,KAAAA;GACbC,cAAcD,KAAAA;GACdE,mBAAmBF,KAAAA;GACnBG,WAAW;GACXC,aAAa;EACf;EAGF,IAAM,CAACF,GAAmBG,KAAW,MAAMC,QAAQC,IAAI,CACrDvB,EAAQI,YAAYC,KAAKmB,qBAAqBZ,MAAAA,GAC9CZ,EAAQI,YAAYiB,QAAQI,WAAWb,MAAM,EAAEJ,WAAWP,EAAOO,UAAU,CAAA,CAAA,CAAGkB,YAAY,IAAA,CAC3F,CAAA,GAEKP,IAAYjB,GAAWW,QAAQvB,MAAM,IACrC8B,IAAcC,GAASD,eAAe;EAE5C,OAAO;GACLjB,YAAYa,KAAAA;GACZZ,YAAYJ,EAAQI;GACpBW,aAAa;IAAEY,MAAM;IAAapC,MAAMW,GAAWW,QAAQtB;GAAK;GAChE0B,cAAcf,GAAWmB;GACzBH;GACAC;GACAX,WAAWP,EAAOO;GAClBY;EACF;CACF;AACF,CAAA"}
|
|
1
|
+
{"version":3,"file":"_projectId-BYccdAtX.mjs","names":["Status","Button","useNavigate","RouteError","error","title","helpText","safeErrorMessage","useLingui","navigate","defaultTitle","t","defaultHelpText","defaultErrorMessage","errorMessage","safeMessage","additionalInfo","status","body","action","variant","onClick","to","createFileRoute","TRPCClientError","ScopeError","type","code","message","currentDomain","id","name","Route","component","lazyRouteComponent","$$splitComponentImporter","errorComponent","$$splitErrorComponentImporter","loader","options","context","params","scopeData","scopeError","trpcClient","auth","setCurrentScope","mutate","projectId","error","currentScope","getCurrentScope","query","domain","data","crumbDomain","undefined","crumbProject","availableServices","accountId","description","project","Promise","all","getAvailableServices","getProject","catch","path"],"sources":["../../src/client/components/Errors/RouteError.tsx","../../src/client/routes/_auth/projects/$projectId.tsx"],"sourcesContent":["import { Status, Button } from \"@cloudoperators/juno-ui-components\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { useNavigate } from \"@tanstack/react-router\"\n\ninterface RouteErrorProps {\n error: unknown\n title?: string\n helpText?: string\n /** Explicitly pass a safe error message to display. Use this when you want to show specific error details. */\n safeErrorMessage?: string\n}\n\n/**\n * Renders errors that occur outside the application layout, such as an expired\n * cookie or token. In these cases, the normal page content is unavailable and\n * only the header may remain visible.\n */\nexport function RouteError({ error, title, helpText, safeErrorMessage }: RouteErrorProps) {\n const { t } = useLingui()\n const navigate = useNavigate()\n\n const defaultTitle = t`Unable to Load Content`\n const defaultHelpText = t`This could be due to insufficient permissions or a temporary service issue. Please check your access rights or try refreshing the page.`\n const defaultErrorMessage = t`An unexpected error occurred.`\n\n // Security: Do not expose raw Error.message by default as it may contain sensitive information.\n // Only display:\n // 1. Explicitly passed safeErrorMessage\n // 2. String errors (when caller explicitly passes a safe string)\n // 3. error.safeMessage if present (for controlled error exposure)\n // 4. Default translated message for all other cases\n const errorMessage =\n safeErrorMessage ||\n (typeof error === \"string\" ? error : null) ||\n (error && typeof error === \"object\" && \"safeMessage\" in error && typeof error.safeMessage === \"string\"\n ? error.safeMessage\n : null) ||\n defaultErrorMessage\n\n const additionalInfo = helpText || defaultHelpText\n\n return (\n <Status\n status=\"error\"\n title={title || defaultTitle}\n body={errorMessage + \" \" + additionalInfo}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Go to Home</Trans>\n </Button>\n }\n />\n )\n}\n","import { createFileRoute, Outlet, useLoaderData, useRouteContext } from \"@tanstack/react-router\"\nimport { AppShell, Button, Container, Stack, Status } from \"@cloudoperators/juno-ui-components\"\nimport { SideNavBar } from \"@/client/routes/_auth/projects/-components/SideNavBar\"\nimport { buildNavSections } from \"@/client/routes/_auth/projects/-components/buildNavSections\"\nimport { Breadcrumbs } from \"@/client/components/Breadcrumbs\"\nimport { RouteError } from \"@/client/components/Errors/RouteError\"\nimport { useMemo } from \"react\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { BreadcrumbExtensionProvider } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\n\n// Type for scope error that we catch and handle gracefully\ntype ScopeError = {\n type: \"scope_error\"\n code: string\n message: string\n currentDomain?: { id: string; name: string } | null\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId\")({\n component: RouteComponent,\n errorComponent: ProjectErrorComponent,\n loader: async (options) => {\n const { context, params } = options\n\n // Catch rescope errors and return them as data instead of throwing\n // This allows the component to render a friendly error UI\n let scopeData\n let scopeError: ScopeError | undefined\n\n try {\n scopeData = await context.trpcClient?.auth.setCurrentScope.mutate({\n type: \"project\",\n projectId: params.projectId || \"\",\n })\n } catch (error) {\n if (error instanceof TRPCClientError) {\n // Try to get the current session's domain to show in the error message\n let currentDomain: { id: string; name: string } | null | undefined\n try {\n const currentScope = await context.trpcClient?.auth.getCurrentScope.query()\n if (currentScope?.domain?.id && currentScope?.domain?.name) {\n currentDomain = { id: currentScope.domain.id, name: currentScope.domain.name }\n }\n } catch {\n // Ignore errors fetching current scope\n }\n\n // Return error as data so the component can handle it\n scopeError = {\n type: \"scope_error\",\n code: error.data?.code || \"UNKNOWN\",\n message: error.message,\n currentDomain,\n }\n } else {\n // Unknown errors should still propagate\n throw error\n }\n }\n\n // If scope failed, return early with error state\n if (scopeError) {\n return {\n scopeError,\n projectId: params.projectId,\n // Provide minimal data for error rendering\n trpcClient: context.trpcClient,\n crumbDomain: undefined,\n crumbProject: undefined,\n availableServices: undefined,\n accountId: \"\",\n description: null,\n }\n }\n\n const [availableServices, project] = await Promise.all([\n context.trpcClient?.auth.getAvailableServices.query(),\n context.trpcClient?.project.getProject.query({ projectId: params.projectId }).catch(() => null),\n ])\n\n const accountId = scopeData?.domain?.id || \"\"\n const description = project?.description ?? null\n\n return {\n scopeError: undefined,\n trpcClient: context.trpcClient,\n crumbDomain: { path: `/projects`, name: scopeData?.domain?.name },\n crumbProject: scopeData?.project,\n availableServices,\n accountId,\n projectId: params.projectId,\n description,\n }\n },\n})\n\nfunction RouteComponent() {\n const { t } = useLingui()\n const loaderData = useLoaderData({ from: Route.id })\n const navigate = Route.useNavigate()\n const { enabledServices, serviceExtensions } = useRouteContext({ strict: false })\n\n const projectName = loaderData.crumbProject?.name || loaderData.projectId\n const projectLabel = loaderData.crumbDomain?.name ? `${loaderData.crumbDomain.name}/${projectName}` : projectName\n useSetBreadcrumb(Route.id, projectLabel)\n\n // Handle scope errors with a friendly UI using Juno's Status component\n if (loaderData.scopeError) {\n const { code, message, currentDomain } = loaderData.scopeError\n const domainName = currentDomain?.name\n\n if (code === \"UNAUTHORIZED\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n title={t`Session Changed`}\n body={t`Your session was changed or ended in another browser tab. This can happen if you logged out or switched to a different account or domain elsewhere. Log in again to continue.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Sign in</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"NOT_FOUND\") {\n const bodyText = domainName\n ? t`This project doesn't exist or is not accessible from your current domain. Your current domain is ${domainName}. Select a project from your current domain.`\n : t`This project doesn't exist or is not accessible from your current domain. Select a project from your current domain.`\n\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Accessible`}\n body={bodyText}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"FORBIDDEN\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={403}\n title={t`Access Denied`}\n body={t`You don't have permission to access this project.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // Fallback for other error codes\n return <RouteError error={new Error(message)} />\n }\n\n const { availableServices, projectId, crumbProject, crumbDomain } = loaderData\n\n const sections = useMemo(\n () => buildNavSections(projectId, availableServices!, enabledServices, serviceExtensions),\n [projectId, availableServices, enabledServices, serviceExtensions]\n )\n\n return (\n <AppShell\n embedded\n sideNavigation={\n <SideNavBar\n sections={sections}\n projectId={projectId}\n projectName={crumbProject?.name || projectId}\n domainName={crumbDomain?.name}\n />\n }\n className=\"h-min-screen\"\n >\n <Container>\n <Stack direction=\"vertical\" distribution=\"start\" alignment=\"stretch\" className=\"xl:flex-row\" gap=\"6\">\n {/* Main content area */}\n <div className=\"min-w-0 flex-1\">\n <BreadcrumbExtensionProvider>\n <Breadcrumbs />\n <Outlet />\n </BreadcrumbExtensionProvider>\n </div>\n </Stack>\n </Container>\n </AppShell>\n )\n}\n\nfunction ProjectErrorComponent({ error }: { error: Error & { statusCode?: number } }) {\n const { t } = useLingui()\n const navigate = Route.useNavigate()\n\n // Handle 404 errors with a custom UI\n if (error.statusCode === 404 || error.message === \"Project not found\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Found`}\n body={t`The project you are looking for doesn't exist or you don't have access to it.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // For all other errors, use the default RouteError component\n return <RouteError error={error} safeErrorMessage={error instanceof TRPCClientError ? error.message : undefined} />\n}\n"],"mappings":";;;;;;AAiBA,SAAgBG,EAAW,EAAEC,UAAOC,UAAOC,aAAUC,uBAAmC;CACtF,IAAM,EAAA,MAAA,GAAA,GAAA,MAAQC,EAAAA,GACRC,IAAWP,EAAAA,GAEXQ,IAAeC,EAAAA,EAAC,EAAA,IAAA,SAAuB,CAAA,GACvCC,IAAkBD,EAAAA,EAAC,EAAA,IAAA,SAAwI,CAAA,GAC3JE,IAAsBF,EAAAA,EAAC,EAAA,IAAA,SAA8B,CAAA,GAQrDG,IACJP,MACC,OAAOH,KAAU,WAAWA,IAAQ,UACpCA,KAAS,OAAOA,KAAU,YAAY,iBAAiBA,KAAS,OAAOA,EAAMW,eAAgB,WAC1FX,EAAMW,cACN,SACJF,GAEIG,IAAiBV,KAAYM;CAEnC,OACE,gBAACZ,GAAAA;EACCiB,QAAO;EACPZ,OAAOA,KAASK;EAChBQ,MAAMJ,IAAe,MAAME;EAC3BG,QACE,gBAAClB,GAAAA;GAAOmB,SAAQ;GAAUC,eAAeZ,EAAS,EAAEa,IAAI,IAAI,CAAA;GAC1D,UAAA,gBAAA,GAAA,EAAA,IAAA,SAAA,CAAA;;;AAKV;ACjCA,IAAaU,IAAQT,EAAgB,4BAAA,CAAA,CAA8B;CACjEU,WAASC,6CAAA,WAAA;CACTE,gBAAcF,6CAAA,gBAAA;CACdI,QAAQ,OAAOC,MAAAA;EACb,IAAM,EAAEC,YAASC,cAAWF,GAIxBG,GACAC;EAEJ,IAAI;GACFD,IAAY,MAAMF,EAAQI,YAAYC,KAAKC,gBAAgBC,OAAO;IAChErB,MAAM;IACNsB,WAAWP,EAAOO,aAAa;GACjC,CAAA;EACF,SAASC,GAAO;GACd,IAAIA,aAAiBzB,GAAiB;IAEpC,IAAIK;IACJ,IAAI;KACF,IAAMqB,IAAe,MAAMV,EAAQI,YAAYC,KAAKM,gBAAgBC,MAAAA;KACpE,AAAIF,GAAcG,QAAQvB,MAAMoB,GAAcG,QAAQtB,SACpDF,IAAgB;MAAEC,IAAIoB,EAAaG,OAAOvB;MAAIC,MAAMmB,EAAaG,OAAOtB;KAAK;IAEjF,QAAQ,CACN;IAIFY,IAAa;KACXjB,MAAM;KACNC,MAAMsB,EAAMK,MAAM3B,QAAQ;KAC1BC,SAASqB,EAAMrB;KACfC;IACF;GACF,OAEE,MAAMoB;EAEV;EAGA,IAAIN,GACF,OAAO;GACLA;GACAK,WAAWP,EAAOO;GAElBJ,YAAYJ,EAAQI;GACpBW,aAAaC,KAAAA;GACbC,cAAcD,KAAAA;GACdE,mBAAmBF,KAAAA;GACnBG,WAAW;GACXC,aAAa;EACf;EAGF,IAAM,CAACF,GAAmBG,KAAW,MAAMC,QAAQC,IAAI,CACrDvB,EAAQI,YAAYC,KAAKmB,qBAAqBZ,MAAAA,GAC9CZ,EAAQI,YAAYiB,QAAQI,WAAWb,MAAM,EAAEJ,WAAWP,EAAOO,UAAU,CAAA,CAAA,CAAGkB,YAAY,IAAA,CAC3F,CAAA,GAEKP,IAAYjB,GAAWW,QAAQvB,MAAM,IACrC8B,IAAcC,GAASD,eAAe;EAE5C,OAAO;GACLjB,YAAYa,KAAAA;GACZZ,YAAYJ,EAAQI;GACpBW,aAAa;IAAEY,MAAM;IAAapC,MAAMW,GAAWW,QAAQtB;GAAK;GAChE0B,cAAcf,GAAWmB;GACzBH;GACAC;GACAX,WAAWP,EAAOO;GAClBY;EACF;CACF;AACF,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as e, G as t, N as n, Q as r, Y as i, _ as a, b as o, i as s, rt as c, ut as l, w as u, z as d } from "./build-WuxXRAqf.mjs";
|
|
2
2
|
import { t as f } from "./Slot-CWb612-_.mjs";
|
|
3
|
-
import { n as p, t as m } from "./_projectId-
|
|
3
|
+
import { n as p, t as m } from "./_projectId-BYccdAtX.mjs";
|
|
4
4
|
import { t as h } from "./helpers-1PpYf-fC.mjs";
|
|
5
5
|
import { t as g } from "./serviceCatalog-B3lKWC9V.mjs";
|
|
6
6
|
import { n as _, t as v } from "./storageProviders-CJ5rOh13.mjs";
|
|
@@ -206,7 +206,7 @@ function U() {
|
|
|
206
206
|
children: /*#__PURE__*/ T(c, {
|
|
207
207
|
status: "error",
|
|
208
208
|
title: n._({ id: "odyhRS" }),
|
|
209
|
-
body: n._({ id: "
|
|
209
|
+
body: n._({ id: "k0nPeO" }),
|
|
210
210
|
action: /*#__PURE__*/ T(s, {
|
|
211
211
|
variant: "primary",
|
|
212
212
|
onClick: () => o({ to: "/" }),
|
|
@@ -216,9 +216,9 @@ function U() {
|
|
|
216
216
|
});
|
|
217
217
|
if (t === "NOT_FOUND") {
|
|
218
218
|
let t = l ? n._({
|
|
219
|
-
id: "+
|
|
219
|
+
id: "J+g1vG",
|
|
220
220
|
values: { domainName: l }
|
|
221
|
-
}) : n._({ id: "
|
|
221
|
+
}) : n._({ id: "O7FpJD" });
|
|
222
222
|
return /*#__PURE__*/ T(e, {
|
|
223
223
|
className: "py-8",
|
|
224
224
|
children: /*#__PURE__*/ T(c, {
|
|
@@ -280,4 +280,4 @@ function U() {
|
|
|
280
280
|
//#endregion
|
|
281
281
|
export { U as component };
|
|
282
282
|
|
|
283
|
-
//# sourceMappingURL=_projectId-
|
|
283
|
+
//# sourceMappingURL=_projectId-CBjZC1tv.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_projectId-ZBk6vka0.mjs","names":["useNavigate","useMatches","useParams","useRouteContext","useState","useEffect","SideNavigation","SideNavigationList","SideNavigationGroup","SideNavigationItem","Divider","isRouteInfo","Slot","SideNavBar","projectId","projectName","domainName","sections","navigate","matches","provider","serviceType","strict","slots","navBadge","service","serviceBadge","component","useShadowDOM","currentService","activeMatch","reverse","find","m","staticData","section","undefined","activeRouteInfo","activeSection","activeService","forceOpenCounter","setForceOpenCounter","Object","fromEntries","map","s","prev","ariaLabel","onClick","to","params","label","p","className","spacing","services","open","item","isStorageContainers","isSelected","span","selected","sideNavBanner","getServiceIndex","hasServiceByName","STORAGE_PROVIDER","STORAGE_TYPE_BY_PROVIDER","buildNavSections","projectId","availableServices","enabledServices","serviceExtensions","serviceIndex","isEnabled","service","includes","computeServices","label","t","navigate","nav","to","params","networkServices","storageServices","SWIFT","provider","storageType","CEPH","sectionMap","Map","section","services","extension","serviceType","serviceName","get","push","values","filter","s","length","Breadcrumb","BreadcrumbItem","JunoBreadcrumbItem","useNavigate","useParams","useContext","useMemo","BreadcrumbExtensionContext","useBreadcrumbs","Breadcrumbs","navigate","projectId","strict","breadcrumbs","extensionCrumbs","allBreadcrumbs","length","slice","active","onClick","to","params","undefined","className","map","item","index","label","icon","Outlet","useLoaderData","useRouteContext","AppShell","Button","Container","Stack","Status","SideNavBar","buildNavSections","Breadcrumbs","RouteError","useMemo","useLingui","Trans","BreadcrumbExtensionProvider","useSetBreadcrumb","ScopeError","type","code","message","currentDomain","id","name","Route","RouteComponent","t","loaderData","from","navigate","useNavigate","enabledServices","serviceExtensions","strict","projectName","crumbProject","projectId","projectLabel","crumbDomain","scopeError","domainName","to","bodyText","Error","availableServices","sections","component"],"sources":["../../src/client/routes/_auth/projects/-components/SideNavBar.tsx","../../src/client/routes/_auth/projects/-components/buildNavSections.ts","../../src/client/components/Breadcrumbs.tsx","../../src/client/routes/_auth/projects/$projectId.tsx?tsr-split=component"],"sourcesContent":["import { useNavigate, useMatches, useParams, useRouteContext } from \"@tanstack/react-router\"\nimport { useState, useEffect } from \"react\"\nimport {\n SideNavigation,\n SideNavigationList,\n SideNavigationGroup,\n SideNavigationItem,\n Divider,\n} from \"@cloudoperators/juno-ui-components/index\"\nimport { isRouteInfo } from \"@/client/routes/routeInfo\"\nimport { Slot } from \"@/client/components/Slot\"\nimport type { NavSection } from \"./buildNavSections\"\n\ninterface SideNavBarProps {\n projectId: string\n projectName: string\n domainName?: string\n sections: NavSection[]\n}\n\nexport const SideNavBar = ({ projectId, projectName, domainName, sections }: SideNavBarProps) => {\n const navigate = useNavigate()\n const matches = useMatches()\n const { provider, serviceType } = useParams({ strict: false }) as { provider?: string; serviceType?: string }\n const { slots } = useRouteContext({ strict: false })\n\n const navBadge = (service: string) => {\n if (!slots?.serviceBadge) return null\n return <Slot component={slots.serviceBadge} useShadowDOM={false} currentService={service} />\n }\n\n // Read active section/service from the deepest match that has meaningful RouteInfo staticData.\n // isRouteInfo uses an all-optional schema, so an empty {} also passes — require at least section\n // or service to be set so that stub routes (index/splat) don't shadow their layout parent.\n const activeMatch = [...matches]\n .reverse()\n .find(\n (m) => isRouteInfo(m.staticData) && (m.staticData.section !== undefined || m.staticData.service !== undefined)\n )\n const activeRouteInfo = activeMatch && isRouteInfo(activeMatch.staticData) ? activeMatch.staticData : undefined\n const activeSection = activeRouteInfo?.section ?? null\n // Extension mount routes carry only staticData.section (no static `service`), so fall back to\n // the $serviceType URL param to identify the active service and highlight its nav item.\n const activeService = activeRouteInfo?.service ?? serviceType ?? null\n\n // Track which sections should be forced open by incrementing a counter\n // This forces a remount of SideNavigationGroup since it doesn't expose onToggle\n const [forceOpenCounter, setForceOpenCounter] = useState<Record<string, number>>(() =>\n Object.fromEntries(sections.map((s) => [s.section, 0]))\n )\n\n // When navigating to a section, force it to expand by remounting\n useEffect(() => {\n if (activeSection) {\n setForceOpenCounter((prev) => ({ ...prev, [activeSection]: (prev[activeSection] || 0) + 1 }))\n }\n }, [activeSection])\n\n return (\n <SideNavigation ariaLabel=\"Project Side Navigation\">\n <>\n <SideNavigationList>\n <>\n <SideNavigationItem\n onClick={() => navigate({ to: \"/projects/$projectId\", params: { projectId } })}\n label={\n <>\n {domainName && <p className=\"text-theme-light text-xs leading-4 font-bold\">{domainName} /</p>}\n <p className=\"leading-5 font-normal\">{projectName}</p>\n </>\n }\n />\n <Divider spacing=\"1\" />\n {sections.map(({ section, label, services }) => (\n <SideNavigationGroup key={`${section}-${forceOpenCounter[section]}`} label={label} open={true}>\n {services.map((item) => {\n const isStorageContainers = activeSection === \"storage\" && activeService === \"containers\"\n const isSelected =\n activeSection === section &&\n (isStorageContainers ? item.params.provider === provider : activeService === item.service)\n\n return (\n <SideNavigationItem\n key={item.service}\n onClick={() => item.navigate(navigate)}\n label={\n <span className=\"flex items-start gap-2\">\n {item.label}\n {navBadge(item.service)}\n </span>\n }\n selected={isSelected}\n />\n )\n })}\n </SideNavigationGroup>\n ))}\n </>\n </SideNavigationList>\n {slots?.sideNavBanner && <Slot component={slots.sideNavBanner} />}\n </>\n </SideNavigation>\n )\n}\n","import { getServiceIndex } from \"@/server/Authentication/helpers\"\nimport { t } from \"@lingui/core/macro\"\nimport type { NavigateFn } from \"@tanstack/react-router\"\nimport type { ServiceExtension } from \"@/client/AuroraApp\"\nimport { hasServiceByName } from \"@/client/utils/serviceCatalog\"\nimport { STORAGE_PROVIDER, STORAGE_TYPE_BY_PROVIDER } from \"@/client/utils/storageProviders\"\n\nexport type NavItem = {\n service: string\n label: string\n navigate: (navigateFn: NavigateFn) => void\n params: Record<string, string>\n}\n\nexport type NavSection = {\n section: string\n label: string\n services: NavItem[]\n}\n\nexport function buildNavSections(\n projectId: string,\n availableServices: { type: string; name: string }[],\n enabledServices?: string[],\n serviceExtensions?: ServiceExtension[]\n): NavSection[] {\n const serviceIndex = getServiceIndex(availableServices)\n const isEnabled = (service: string) => !enabledServices || enabledServices.includes(service)\n\n const computeServices: NavItem[] = [\n ...(serviceIndex[\"image\"]?.[\"glance\"] && isEnabled(\"images\")\n ? [\n {\n service: \"images\",\n label: t`Images`,\n navigate: (nav: NavigateFn) => nav({ to: \"/projects/$projectId/compute/images\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ...(serviceIndex?.[\"compute\"]?.[\"nova\"] && isEnabled(\"flavors\")\n ? [\n {\n service: \"flavors\",\n label: t`Flavors`,\n navigate: (nav: NavigateFn) => nav({ to: \"/projects/$projectId/compute/flavors\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ]\n\n const networkServices: NavItem[] = serviceIndex[\"network\"]\n ? [\n ...(isEnabled(\"securitygroups\")\n ? [\n {\n service: \"securitygroups\",\n label: t`Security Groups`,\n navigate: (nav: NavigateFn) =>\n nav({ to: \"/projects/$projectId/network/securitygroups\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ...(isEnabled(\"floatingips\")\n ? [\n {\n service: \"floatingips\",\n label: t`Floating IPs`,\n navigate: (nav: NavigateFn) =>\n nav({ to: \"/projects/$projectId/network/floatingips\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ]\n : []\n\n const storageServices: NavItem[] = [\n ...(hasServiceByName(serviceIndex, STORAGE_PROVIDER.SWIFT) && isEnabled(\"containers\")\n ? [\n {\n service: \"containers\",\n label: t`Object Storage (Swift)`,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/storage/$provider/$storageType\",\n params: {\n projectId,\n provider: STORAGE_PROVIDER.SWIFT,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.SWIFT],\n },\n }),\n params: {\n projectId,\n provider: STORAGE_PROVIDER.SWIFT,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.SWIFT],\n },\n },\n ]\n : []),\n ...(hasServiceByName(serviceIndex, STORAGE_PROVIDER.CEPH) && isEnabled(\"ceph-containers\")\n ? [\n {\n service: \"ceph-containers\",\n label: t`Object Storage (Ceph)`,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/storage/$provider/$storageType\",\n params: {\n projectId,\n provider: STORAGE_PROVIDER.CEPH,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.CEPH],\n },\n }),\n params: {\n projectId,\n provider: STORAGE_PROVIDER.CEPH,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.CEPH],\n },\n },\n ]\n : []),\n ]\n\n // Seed the section map with built-in sections\n const sectionMap = new Map<string, NavSection>([\n [\"compute\", { section: \"compute\", label: t`Compute`, services: computeServices }],\n [\"network\", { section: \"network\", label: t`Network`, services: networkServices }],\n [\"storage\", { section: \"storage\", label: t`Storage`, services: storageServices }],\n [\"services\", { section: \"services\", label: t`Services`, services: [] }],\n ])\n\n // Merge service extensions: activate when the service exists in the project's catalog\n for (const extension of serviceExtensions ?? []) {\n if (!serviceIndex[extension.serviceType]?.[extension.serviceName]) continue\n if (enabledServices && !enabledServices.includes(extension.serviceType)) continue\n\n sectionMap.get(\"services\")?.services.push({\n service: extension.serviceType,\n label: extension.label,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/services/$serviceType\",\n params: { projectId, serviceType: extension.serviceType },\n }),\n params: { projectId, serviceType: extension.serviceType },\n })\n }\n\n return [...sectionMap.values()].filter((s) => s.services.length > 0)\n}\n","import { Breadcrumb, BreadcrumbItem as JunoBreadcrumbItem } from \"@cloudoperators/juno-ui-components\"\nimport type { KnownIcons } from \"@cloudoperators/juno-ui-components\"\nimport { useNavigate, useParams } from \"@tanstack/react-router\"\nimport { useContext, useMemo } from \"react\"\nimport { BreadcrumbExtensionContext } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useBreadcrumbs, type BreadcrumbItem } from \"@/client/hooks/useBreadcrumbs\"\n\nexport function Breadcrumbs() {\n const navigate = useNavigate()\n const { projectId } = useParams({ strict: false }) as { projectId?: string }\n const { breadcrumbs: extensionCrumbs } = useContext(BreadcrumbExtensionContext)\n const breadcrumbs = useBreadcrumbs()\n\n const allBreadcrumbs = useMemo((): BreadcrumbItem[] => {\n if (extensionCrumbs.length === 0 || breadcrumbs.length === 0) return breadcrumbs\n\n // Extension crumbs present: deactivate the last OSS crumb (always the project crumb on\n // /services/* routes) and append the extension crumbs pushed by the sub-app.\n return [\n ...breadcrumbs.slice(0, -1),\n {\n ...breadcrumbs[breadcrumbs.length - 1],\n active: false,\n onClick: projectId\n ? () => navigate({ to: \"/projects/$projectId\" as never, params: { projectId } as never })\n : undefined,\n },\n ...extensionCrumbs,\n ]\n }, [breadcrumbs, extensionCrumbs, projectId, navigate])\n\n return (\n <Breadcrumb className=\"relative z-1 mt-8 mb-4\">\n {allBreadcrumbs.map((item, index) => (\n <JunoBreadcrumbItem\n key={index}\n label={item.label}\n icon={item.icon as KnownIcons | undefined}\n onClick={item.onClick}\n active={item.active}\n />\n ))}\n </Breadcrumb>\n )\n}\n","import { createFileRoute, Outlet, useLoaderData, useRouteContext } from \"@tanstack/react-router\"\nimport { AppShell, Button, Container, Stack, Status } from \"@cloudoperators/juno-ui-components\"\nimport { SideNavBar } from \"@/client/routes/_auth/projects/-components/SideNavBar\"\nimport { buildNavSections } from \"@/client/routes/_auth/projects/-components/buildNavSections\"\nimport { Breadcrumbs } from \"@/client/components/Breadcrumbs\"\nimport { RouteError } from \"@/client/components/Errors/RouteError\"\nimport { useMemo } from \"react\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { BreadcrumbExtensionProvider } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\n\n// Type for scope error that we catch and handle gracefully\ntype ScopeError = {\n type: \"scope_error\"\n code: string\n message: string\n currentDomain?: { id: string; name: string } | null\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId\")({\n component: RouteComponent,\n errorComponent: ProjectErrorComponent,\n loader: async (options) => {\n const { context, params } = options\n\n // Catch rescope errors and return them as data instead of throwing\n // This allows the component to render a friendly error UI\n let scopeData\n let scopeError: ScopeError | undefined\n\n try {\n scopeData = await context.trpcClient?.auth.setCurrentScope.mutate({\n type: \"project\",\n projectId: params.projectId || \"\",\n })\n } catch (error) {\n if (error instanceof TRPCClientError) {\n // Try to get the current session's domain to show in the error message\n let currentDomain: { id: string; name: string } | null | undefined\n try {\n const currentScope = await context.trpcClient?.auth.getCurrentScope.query()\n if (currentScope?.domain?.id && currentScope?.domain?.name) {\n currentDomain = { id: currentScope.domain.id, name: currentScope.domain.name }\n }\n } catch {\n // Ignore errors fetching current scope\n }\n\n // Return error as data so the component can handle it\n scopeError = {\n type: \"scope_error\",\n code: error.data?.code || \"UNKNOWN\",\n message: error.message,\n currentDomain,\n }\n } else {\n // Unknown errors should still propagate\n throw error\n }\n }\n\n // If scope failed, return early with error state\n if (scopeError) {\n return {\n scopeError,\n projectId: params.projectId,\n // Provide minimal data for error rendering\n trpcClient: context.trpcClient,\n crumbDomain: undefined,\n crumbProject: undefined,\n availableServices: undefined,\n accountId: \"\",\n description: null,\n }\n }\n\n const [availableServices, project] = await Promise.all([\n context.trpcClient?.auth.getAvailableServices.query(),\n context.trpcClient?.project.getProject.query({ projectId: params.projectId }).catch(() => null),\n ])\n\n const accountId = scopeData?.domain?.id || \"\"\n const description = project?.description ?? null\n\n return {\n scopeError: undefined,\n trpcClient: context.trpcClient,\n crumbDomain: { path: `/projects`, name: scopeData?.domain?.name },\n crumbProject: scopeData?.project,\n availableServices,\n accountId,\n projectId: params.projectId,\n description,\n }\n },\n})\n\nfunction RouteComponent() {\n const { t } = useLingui()\n const loaderData = useLoaderData({ from: Route.id })\n const navigate = Route.useNavigate()\n const { enabledServices, serviceExtensions } = useRouteContext({ strict: false })\n\n const projectName = loaderData.crumbProject?.name || loaderData.projectId\n const projectLabel = loaderData.crumbDomain?.name ? `${loaderData.crumbDomain.name}/${projectName}` : projectName\n useSetBreadcrumb(Route.id, projectLabel)\n\n // Handle scope errors with a friendly UI using Juno's Status component\n if (loaderData.scopeError) {\n const { code, message, currentDomain } = loaderData.scopeError\n const domainName = currentDomain?.name\n\n if (code === \"UNAUTHORIZED\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n title={t`Session Changed`}\n body={t`Your session was changed or ended in another browser tab. This can happen if you logged out or switched to a different account or domain elsewhere. Please log in again to continue.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Sign in</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"NOT_FOUND\") {\n const bodyText = domainName\n ? t`This project doesn't exist or is not accessible from your current domain. Your current domain is ${domainName}. Please select a project from your current domain.`\n : t`This project doesn't exist or is not accessible from your current domain. Please select a project from your current domain.`\n\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Accessible`}\n body={bodyText}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"FORBIDDEN\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={403}\n title={t`Access Denied`}\n body={t`You don't have permission to access this project.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // Fallback for other error codes\n return <RouteError error={new Error(message)} />\n }\n\n const { availableServices, projectId, crumbProject, crumbDomain } = loaderData\n\n const sections = useMemo(\n () => buildNavSections(projectId, availableServices!, enabledServices, serviceExtensions),\n [projectId, availableServices, enabledServices, serviceExtensions]\n )\n\n return (\n <AppShell\n embedded\n sideNavigation={\n <SideNavBar\n sections={sections}\n projectId={projectId}\n projectName={crumbProject?.name || projectId}\n domainName={crumbDomain?.name}\n />\n }\n className=\"h-min-screen\"\n >\n <Container>\n <Stack direction=\"vertical\" distribution=\"start\" alignment=\"stretch\" className=\"xl:flex-row\" gap=\"6\">\n {/* Main content area */}\n <div className=\"min-w-0 flex-1\">\n <BreadcrumbExtensionProvider>\n <Breadcrumbs />\n <Outlet />\n </BreadcrumbExtensionProvider>\n </div>\n </Stack>\n </Container>\n </AppShell>\n )\n}\n\nfunction ProjectErrorComponent({ error }: { error: Error & { statusCode?: number } }) {\n const { t } = useLingui()\n const navigate = Route.useNavigate()\n\n // Handle 404 errors with a custom UI\n if (error.statusCode === 404 || error.message === \"Project not found\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Found`}\n body={t`The project you are looking for doesn't exist or you don't have access to it.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // For all other errors, use the default RouteError component\n return <RouteError error={error} safeErrorMessage={error instanceof TRPCClientError ? error.message : undefined} />\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"_projectId-CBjZC1tv.mjs","names":["useNavigate","useMatches","useParams","useRouteContext","useState","useEffect","SideNavigation","SideNavigationList","SideNavigationGroup","SideNavigationItem","Divider","isRouteInfo","Slot","SideNavBar","projectId","projectName","domainName","sections","navigate","matches","provider","serviceType","strict","slots","navBadge","service","serviceBadge","component","useShadowDOM","currentService","activeMatch","reverse","find","m","staticData","section","undefined","activeRouteInfo","activeSection","activeService","forceOpenCounter","setForceOpenCounter","Object","fromEntries","map","s","prev","ariaLabel","onClick","to","params","label","p","className","spacing","services","open","item","isStorageContainers","isSelected","span","selected","sideNavBanner","getServiceIndex","hasServiceByName","STORAGE_PROVIDER","STORAGE_TYPE_BY_PROVIDER","buildNavSections","projectId","availableServices","enabledServices","serviceExtensions","serviceIndex","isEnabled","service","includes","computeServices","label","t","navigate","nav","to","params","networkServices","storageServices","SWIFT","provider","storageType","CEPH","sectionMap","Map","section","services","extension","serviceType","serviceName","get","push","values","filter","s","length","Breadcrumb","BreadcrumbItem","JunoBreadcrumbItem","useNavigate","useParams","useContext","useMemo","BreadcrumbExtensionContext","useBreadcrumbs","Breadcrumbs","navigate","projectId","strict","breadcrumbs","extensionCrumbs","allBreadcrumbs","length","slice","active","onClick","to","params","undefined","className","map","item","index","label","icon","Outlet","useLoaderData","useRouteContext","AppShell","Button","Container","Stack","Status","SideNavBar","buildNavSections","Breadcrumbs","RouteError","useMemo","useLingui","Trans","BreadcrumbExtensionProvider","useSetBreadcrumb","ScopeError","type","code","message","currentDomain","id","name","Route","RouteComponent","t","loaderData","from","navigate","useNavigate","enabledServices","serviceExtensions","strict","projectName","crumbProject","projectId","projectLabel","crumbDomain","scopeError","domainName","to","bodyText","Error","availableServices","sections","component"],"sources":["../../src/client/routes/_auth/projects/-components/SideNavBar.tsx","../../src/client/routes/_auth/projects/-components/buildNavSections.ts","../../src/client/components/Breadcrumbs.tsx","../../src/client/routes/_auth/projects/$projectId.tsx?tsr-split=component"],"sourcesContent":["import { useNavigate, useMatches, useParams, useRouteContext } from \"@tanstack/react-router\"\nimport { useState, useEffect } from \"react\"\nimport {\n SideNavigation,\n SideNavigationList,\n SideNavigationGroup,\n SideNavigationItem,\n Divider,\n} from \"@cloudoperators/juno-ui-components/index\"\nimport { isRouteInfo } from \"@/client/routes/routeInfo\"\nimport { Slot } from \"@/client/components/Slot\"\nimport type { NavSection } from \"./buildNavSections\"\n\ninterface SideNavBarProps {\n projectId: string\n projectName: string\n domainName?: string\n sections: NavSection[]\n}\n\nexport const SideNavBar = ({ projectId, projectName, domainName, sections }: SideNavBarProps) => {\n const navigate = useNavigate()\n const matches = useMatches()\n const { provider, serviceType } = useParams({ strict: false }) as { provider?: string; serviceType?: string }\n const { slots } = useRouteContext({ strict: false })\n\n const navBadge = (service: string) => {\n if (!slots?.serviceBadge) return null\n return <Slot component={slots.serviceBadge} useShadowDOM={false} currentService={service} />\n }\n\n // Read active section/service from the deepest match that has meaningful RouteInfo staticData.\n // isRouteInfo uses an all-optional schema, so an empty {} also passes — require at least section\n // or service to be set so that stub routes (index/splat) don't shadow their layout parent.\n const activeMatch = [...matches]\n .reverse()\n .find(\n (m) => isRouteInfo(m.staticData) && (m.staticData.section !== undefined || m.staticData.service !== undefined)\n )\n const activeRouteInfo = activeMatch && isRouteInfo(activeMatch.staticData) ? activeMatch.staticData : undefined\n const activeSection = activeRouteInfo?.section ?? null\n // Extension mount routes carry only staticData.section (no static `service`), so fall back to\n // the $serviceType URL param to identify the active service and highlight its nav item.\n const activeService = activeRouteInfo?.service ?? serviceType ?? null\n\n // Track which sections should be forced open by incrementing a counter\n // This forces a remount of SideNavigationGroup since it doesn't expose onToggle\n const [forceOpenCounter, setForceOpenCounter] = useState<Record<string, number>>(() =>\n Object.fromEntries(sections.map((s) => [s.section, 0]))\n )\n\n // When navigating to a section, force it to expand by remounting\n useEffect(() => {\n if (activeSection) {\n setForceOpenCounter((prev) => ({ ...prev, [activeSection]: (prev[activeSection] || 0) + 1 }))\n }\n }, [activeSection])\n\n return (\n <SideNavigation ariaLabel=\"Project Side Navigation\">\n <>\n <SideNavigationList>\n <>\n <SideNavigationItem\n onClick={() => navigate({ to: \"/projects/$projectId\", params: { projectId } })}\n label={\n <>\n {domainName && <p className=\"text-theme-light text-xs leading-4 font-bold\">{domainName} /</p>}\n <p className=\"leading-5 font-normal\">{projectName}</p>\n </>\n }\n />\n <Divider spacing=\"1\" />\n {sections.map(({ section, label, services }) => (\n <SideNavigationGroup key={`${section}-${forceOpenCounter[section]}`} label={label} open={true}>\n {services.map((item) => {\n const isStorageContainers = activeSection === \"storage\" && activeService === \"containers\"\n const isSelected =\n activeSection === section &&\n (isStorageContainers ? item.params.provider === provider : activeService === item.service)\n\n return (\n <SideNavigationItem\n key={item.service}\n onClick={() => item.navigate(navigate)}\n label={\n <span className=\"flex items-start gap-2\">\n {item.label}\n {navBadge(item.service)}\n </span>\n }\n selected={isSelected}\n />\n )\n })}\n </SideNavigationGroup>\n ))}\n </>\n </SideNavigationList>\n {slots?.sideNavBanner && <Slot component={slots.sideNavBanner} />}\n </>\n </SideNavigation>\n )\n}\n","import { getServiceIndex } from \"@/server/Authentication/helpers\"\nimport { t } from \"@lingui/core/macro\"\nimport type { NavigateFn } from \"@tanstack/react-router\"\nimport type { ServiceExtension } from \"@/client/AuroraApp\"\nimport { hasServiceByName } from \"@/client/utils/serviceCatalog\"\nimport { STORAGE_PROVIDER, STORAGE_TYPE_BY_PROVIDER } from \"@/client/utils/storageProviders\"\n\nexport type NavItem = {\n service: string\n label: string\n navigate: (navigateFn: NavigateFn) => void\n params: Record<string, string>\n}\n\nexport type NavSection = {\n section: string\n label: string\n services: NavItem[]\n}\n\nexport function buildNavSections(\n projectId: string,\n availableServices: { type: string; name: string }[],\n enabledServices?: string[],\n serviceExtensions?: ServiceExtension[]\n): NavSection[] {\n const serviceIndex = getServiceIndex(availableServices)\n const isEnabled = (service: string) => !enabledServices || enabledServices.includes(service)\n\n const computeServices: NavItem[] = [\n ...(serviceIndex[\"image\"]?.[\"glance\"] && isEnabled(\"images\")\n ? [\n {\n service: \"images\",\n label: t`Images`,\n navigate: (nav: NavigateFn) => nav({ to: \"/projects/$projectId/compute/images\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ...(serviceIndex?.[\"compute\"]?.[\"nova\"] && isEnabled(\"flavors\")\n ? [\n {\n service: \"flavors\",\n label: t`Flavors`,\n navigate: (nav: NavigateFn) => nav({ to: \"/projects/$projectId/compute/flavors\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ]\n\n const networkServices: NavItem[] = serviceIndex[\"network\"]\n ? [\n ...(isEnabled(\"securitygroups\")\n ? [\n {\n service: \"securitygroups\",\n label: t`Security Groups`,\n navigate: (nav: NavigateFn) =>\n nav({ to: \"/projects/$projectId/network/securitygroups\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ...(isEnabled(\"floatingips\")\n ? [\n {\n service: \"floatingips\",\n label: t`Floating IPs`,\n navigate: (nav: NavigateFn) =>\n nav({ to: \"/projects/$projectId/network/floatingips\", params: { projectId } }),\n params: { projectId },\n },\n ]\n : []),\n ]\n : []\n\n const storageServices: NavItem[] = [\n ...(hasServiceByName(serviceIndex, STORAGE_PROVIDER.SWIFT) && isEnabled(\"containers\")\n ? [\n {\n service: \"containers\",\n label: t`Object Storage (Swift)`,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/storage/$provider/$storageType\",\n params: {\n projectId,\n provider: STORAGE_PROVIDER.SWIFT,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.SWIFT],\n },\n }),\n params: {\n projectId,\n provider: STORAGE_PROVIDER.SWIFT,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.SWIFT],\n },\n },\n ]\n : []),\n ...(hasServiceByName(serviceIndex, STORAGE_PROVIDER.CEPH) && isEnabled(\"ceph-containers\")\n ? [\n {\n service: \"ceph-containers\",\n label: t`Object Storage (Ceph)`,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/storage/$provider/$storageType\",\n params: {\n projectId,\n provider: STORAGE_PROVIDER.CEPH,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.CEPH],\n },\n }),\n params: {\n projectId,\n provider: STORAGE_PROVIDER.CEPH,\n storageType: STORAGE_TYPE_BY_PROVIDER[STORAGE_PROVIDER.CEPH],\n },\n },\n ]\n : []),\n ]\n\n // Seed the section map with built-in sections\n const sectionMap = new Map<string, NavSection>([\n [\"compute\", { section: \"compute\", label: t`Compute`, services: computeServices }],\n [\"network\", { section: \"network\", label: t`Network`, services: networkServices }],\n [\"storage\", { section: \"storage\", label: t`Storage`, services: storageServices }],\n [\"services\", { section: \"services\", label: t`Services`, services: [] }],\n ])\n\n // Merge service extensions: activate when the service exists in the project's catalog\n for (const extension of serviceExtensions ?? []) {\n if (!serviceIndex[extension.serviceType]?.[extension.serviceName]) continue\n if (enabledServices && !enabledServices.includes(extension.serviceType)) continue\n\n sectionMap.get(\"services\")?.services.push({\n service: extension.serviceType,\n label: extension.label,\n navigate: (nav: NavigateFn) =>\n nav({\n to: \"/projects/$projectId/services/$serviceType\",\n params: { projectId, serviceType: extension.serviceType },\n }),\n params: { projectId, serviceType: extension.serviceType },\n })\n }\n\n return [...sectionMap.values()].filter((s) => s.services.length > 0)\n}\n","import { Breadcrumb, BreadcrumbItem as JunoBreadcrumbItem } from \"@cloudoperators/juno-ui-components\"\nimport type { KnownIcons } from \"@cloudoperators/juno-ui-components\"\nimport { useNavigate, useParams } from \"@tanstack/react-router\"\nimport { useContext, useMemo } from \"react\"\nimport { BreadcrumbExtensionContext } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useBreadcrumbs, type BreadcrumbItem } from \"@/client/hooks/useBreadcrumbs\"\n\nexport function Breadcrumbs() {\n const navigate = useNavigate()\n const { projectId } = useParams({ strict: false }) as { projectId?: string }\n const { breadcrumbs: extensionCrumbs } = useContext(BreadcrumbExtensionContext)\n const breadcrumbs = useBreadcrumbs()\n\n const allBreadcrumbs = useMemo((): BreadcrumbItem[] => {\n if (extensionCrumbs.length === 0 || breadcrumbs.length === 0) return breadcrumbs\n\n // Extension crumbs present: deactivate the last OSS crumb (always the project crumb on\n // /services/* routes) and append the extension crumbs pushed by the sub-app.\n return [\n ...breadcrumbs.slice(0, -1),\n {\n ...breadcrumbs[breadcrumbs.length - 1],\n active: false,\n onClick: projectId\n ? () => navigate({ to: \"/projects/$projectId\" as never, params: { projectId } as never })\n : undefined,\n },\n ...extensionCrumbs,\n ]\n }, [breadcrumbs, extensionCrumbs, projectId, navigate])\n\n return (\n <Breadcrumb className=\"relative z-1 mt-8 mb-4\">\n {allBreadcrumbs.map((item, index) => (\n <JunoBreadcrumbItem\n key={index}\n label={item.label}\n icon={item.icon as KnownIcons | undefined}\n onClick={item.onClick}\n active={item.active}\n />\n ))}\n </Breadcrumb>\n )\n}\n","import { createFileRoute, Outlet, useLoaderData, useRouteContext } from \"@tanstack/react-router\"\nimport { AppShell, Button, Container, Stack, Status } from \"@cloudoperators/juno-ui-components\"\nimport { SideNavBar } from \"@/client/routes/_auth/projects/-components/SideNavBar\"\nimport { buildNavSections } from \"@/client/routes/_auth/projects/-components/buildNavSections\"\nimport { Breadcrumbs } from \"@/client/components/Breadcrumbs\"\nimport { RouteError } from \"@/client/components/Errors/RouteError\"\nimport { useMemo } from \"react\"\nimport { TRPCClientError } from \"@trpc/client\"\nimport { useLingui, Trans } from \"@lingui/react/macro\"\nimport { BreadcrumbExtensionProvider } from \"@/client/context/BreadcrumbExtensionContext\"\nimport { useSetBreadcrumb } from \"@/client/hooks/useSetBreadcrumb\"\n\n// Type for scope error that we catch and handle gracefully\ntype ScopeError = {\n type: \"scope_error\"\n code: string\n message: string\n currentDomain?: { id: string; name: string } | null\n}\n\nexport const Route = createFileRoute(\"/_auth/projects/$projectId\")({\n component: RouteComponent,\n errorComponent: ProjectErrorComponent,\n loader: async (options) => {\n const { context, params } = options\n\n // Catch rescope errors and return them as data instead of throwing\n // This allows the component to render a friendly error UI\n let scopeData\n let scopeError: ScopeError | undefined\n\n try {\n scopeData = await context.trpcClient?.auth.setCurrentScope.mutate({\n type: \"project\",\n projectId: params.projectId || \"\",\n })\n } catch (error) {\n if (error instanceof TRPCClientError) {\n // Try to get the current session's domain to show in the error message\n let currentDomain: { id: string; name: string } | null | undefined\n try {\n const currentScope = await context.trpcClient?.auth.getCurrentScope.query()\n if (currentScope?.domain?.id && currentScope?.domain?.name) {\n currentDomain = { id: currentScope.domain.id, name: currentScope.domain.name }\n }\n } catch {\n // Ignore errors fetching current scope\n }\n\n // Return error as data so the component can handle it\n scopeError = {\n type: \"scope_error\",\n code: error.data?.code || \"UNKNOWN\",\n message: error.message,\n currentDomain,\n }\n } else {\n // Unknown errors should still propagate\n throw error\n }\n }\n\n // If scope failed, return early with error state\n if (scopeError) {\n return {\n scopeError,\n projectId: params.projectId,\n // Provide minimal data for error rendering\n trpcClient: context.trpcClient,\n crumbDomain: undefined,\n crumbProject: undefined,\n availableServices: undefined,\n accountId: \"\",\n description: null,\n }\n }\n\n const [availableServices, project] = await Promise.all([\n context.trpcClient?.auth.getAvailableServices.query(),\n context.trpcClient?.project.getProject.query({ projectId: params.projectId }).catch(() => null),\n ])\n\n const accountId = scopeData?.domain?.id || \"\"\n const description = project?.description ?? null\n\n return {\n scopeError: undefined,\n trpcClient: context.trpcClient,\n crumbDomain: { path: `/projects`, name: scopeData?.domain?.name },\n crumbProject: scopeData?.project,\n availableServices,\n accountId,\n projectId: params.projectId,\n description,\n }\n },\n})\n\nfunction RouteComponent() {\n const { t } = useLingui()\n const loaderData = useLoaderData({ from: Route.id })\n const navigate = Route.useNavigate()\n const { enabledServices, serviceExtensions } = useRouteContext({ strict: false })\n\n const projectName = loaderData.crumbProject?.name || loaderData.projectId\n const projectLabel = loaderData.crumbDomain?.name ? `${loaderData.crumbDomain.name}/${projectName}` : projectName\n useSetBreadcrumb(Route.id, projectLabel)\n\n // Handle scope errors with a friendly UI using Juno's Status component\n if (loaderData.scopeError) {\n const { code, message, currentDomain } = loaderData.scopeError\n const domainName = currentDomain?.name\n\n if (code === \"UNAUTHORIZED\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n title={t`Session Changed`}\n body={t`Your session was changed or ended in another browser tab. This can happen if you logged out or switched to a different account or domain elsewhere. Log in again to continue.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/\" })}>\n <Trans>Sign in</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"NOT_FOUND\") {\n const bodyText = domainName\n ? t`This project doesn't exist or is not accessible from your current domain. Your current domain is ${domainName}. Select a project from your current domain.`\n : t`This project doesn't exist or is not accessible from your current domain. Select a project from your current domain.`\n\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Accessible`}\n body={bodyText}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n if (code === \"FORBIDDEN\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={403}\n title={t`Access Denied`}\n body={t`You don't have permission to access this project.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // Fallback for other error codes\n return <RouteError error={new Error(message)} />\n }\n\n const { availableServices, projectId, crumbProject, crumbDomain } = loaderData\n\n const sections = useMemo(\n () => buildNavSections(projectId, availableServices!, enabledServices, serviceExtensions),\n [projectId, availableServices, enabledServices, serviceExtensions]\n )\n\n return (\n <AppShell\n embedded\n sideNavigation={\n <SideNavBar\n sections={sections}\n projectId={projectId}\n projectName={crumbProject?.name || projectId}\n domainName={crumbDomain?.name}\n />\n }\n className=\"h-min-screen\"\n >\n <Container>\n <Stack direction=\"vertical\" distribution=\"start\" alignment=\"stretch\" className=\"xl:flex-row\" gap=\"6\">\n {/* Main content area */}\n <div className=\"min-w-0 flex-1\">\n <BreadcrumbExtensionProvider>\n <Breadcrumbs />\n <Outlet />\n </BreadcrumbExtensionProvider>\n </div>\n </Stack>\n </Container>\n </AppShell>\n )\n}\n\nfunction ProjectErrorComponent({ error }: { error: Error & { statusCode?: number } }) {\n const { t } = useLingui()\n const navigate = Route.useNavigate()\n\n // Handle 404 errors with a custom UI\n if (error.statusCode === 404 || error.message === \"Project not found\") {\n return (\n <Container className=\"py-8\">\n <Status\n status=\"error\"\n code={404}\n title={t`Project Not Found`}\n body={t`The project you are looking for doesn't exist or you don't have access to it.`}\n action={\n <Button variant=\"primary\" onClick={() => navigate({ to: \"/projects\" })}>\n <Trans>Go to Projects</Trans>\n </Button>\n }\n />\n </Container>\n )\n }\n\n // For all other errors, use the default RouteError component\n return <RouteError error={error} safeErrorMessage={error instanceof TRPCClientError ? error.message : undefined} />\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { A as e, i as t, rt as n } from "./build-WuxXRAqf.mjs";
|
|
2
|
-
import { n as r, t as i } from "./_projectId-
|
|
2
|
+
import { n as r, t as i } from "./_projectId-BYccdAtX.mjs";
|
|
3
3
|
import { jsx as a } from "react/jsx-runtime";
|
|
4
4
|
import { TRPCClientError as o } from "@trpc/client";
|
|
5
5
|
import { Trans as s, useLingui as c } from "@lingui/react";
|
|
@@ -27,4 +27,4 @@ function l({ error: l }) {
|
|
|
27
27
|
//#endregion
|
|
28
28
|
export { l as errorComponent };
|
|
29
29
|
|
|
30
|
-
//# sourceMappingURL=_projectId-
|
|
30
|
+
//# sourceMappingURL=_projectId-CLEa1zgw.mjs.map
|