Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const ImageRegistriesList = lazy(
async () => import("pages/images/ImageRegistriesList"),
);

const ImageRegistryDetail = lazy(
async () => import("pages/images/ImageRegistryDetail"),
);
const StorageVolumeDetail = lazy(
async () => import("pages/storage/StorageVolumeDetail"),
);
Expand All @@ -125,7 +128,11 @@ const HOME_REDIRECT_PATHS = [
`${ROOT_PATH}/ui/project`,
];

const App: FC = () => {
interface Props {
projectName: string;
}

const App: FC<Props> = ({ projectName }) => {
const {
defaultProject,
hasNoProjects,
Expand Down Expand Up @@ -520,6 +527,22 @@ const App: FC = () => {
path={`${ROOT_PATH}/ui/image-registries`}
element={<ProtectedRoute outlet={<ImageRegistriesList />} />}
/>
<Route
path={`${ROOT_PATH}/ui/image-registry/:name`}
element={
<ProtectedRoute
outlet={<ImageRegistryDetail projectName={projectName} />}
/>
}
/>
<Route
path={`${ROOT_PATH}/ui/image-registry/:name/:activeTab`}
element={
<ProtectedRoute
outlet={<ImageRegistryDetail projectName={projectName} />}
/>
}
/>
<Route
path={`${ROOT_PATH}/ui/server`}
element={<ProtectedRoute outlet={<Server />} />}
Expand Down
39 changes: 35 additions & 4 deletions src/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
import type { FC } from "react";
import Navigation from "components/Navigation";
import { useState, type FC } from "react";
import Navigation, { ALL_PROJECTS } from "components/Navigation";
import { Application, SkipLink } from "@canonical/react-components";
import Events from "pages/instances/Events";
import App from "./App";
import ErrorBoundary from "components/ErrorBoundary";
import ErrorPage from "components/ErrorPage";
import StatusBar from "components/StatusBar";
import AppProviders from "context/appProviders";
import { useCurrentProject } from "context/useCurrentProject";
import type { LxdProject } from "types/project";

const Root: FC = () => {
const {
project,
isAllProjects: isAllProjectsFromUrl,
isLoading,
} = useCurrentProject();

const initializeProjectName = (
isAllProjectsFromUrl: boolean,
isLoading: boolean,
project: LxdProject | undefined,
) => {
if (isAllProjectsFromUrl) {
return ALL_PROJECTS;
}

if (project && !isLoading) {
return project.name;
}

return "default";
};

const [projectName, setProjectName] = useState(
initializeProjectName(isAllProjectsFromUrl, isLoading, project),
);

return (
<ErrorBoundary fallback={ErrorPage}>
<AppProviders>
<Application id="l-application">
<SkipLink mainId="main-content" />
<Navigation />
<Navigation
projectName={projectName}
setProjectName={setProjectName}
/>
<ErrorBoundary fallback={ErrorPage}>
<App />
<App projectName={projectName} />
<Events />
<StatusBar />
</ErrorBoundary>
Expand Down
36 changes: 35 additions & 1 deletion src/api/image-registries.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { handleResponse } from "util/helpers";
import type { LxdImageRegistry } from "types/image";
import type { LxdImage, LxdImageRegistry } from "types/image";
import type { LxdApiResponse } from "types/apiResponse";
import { addEntitlements } from "util/entitlements/api";
import { ROOT_PATH } from "util/rootPath";
Expand Down Expand Up @@ -34,3 +34,37 @@ export const fetchImageRegistry = async (
return data.metadata;
});
};

export const fetchRegistryImages = async (
name: string,
): Promise<LxdImage[]> => {
return fetch(
`${ROOT_PATH}/1.0/image-registries/${encodeURIComponent(name)}/images`,
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdImage[]>) => {
return data.metadata;
});
};

export const renameImageRegistry = async (
oldName: string,
newName: string,
): Promise<void> => {
await fetch(
`${ROOT_PATH}/1.0/image-registries/${encodeURIComponent(oldName)}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: newName }),
},
).then(handleResponse);
};

export const deleteImageRegistry = async (name: string): Promise<void> => {
await fetch(`${ROOT_PATH}/1.0/image-registries/${encodeURIComponent(name)}`, {
method: "DELETE",
}).then(handleResponse);
};
30 changes: 8 additions & 22 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import type { Location } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { useLoggedInUser } from "context/useLoggedInUser";
import { useSettings } from "context/useSettings";
import type { LxdProject } from "types/project";
import { useIsScreenBelow } from "context/useIsScreenBelow";
import { useIsClustered } from "context/useIsClustered";
import { getReportBugURL } from "util/reportBug";
Expand All @@ -46,6 +45,7 @@ const initialiseOpenNavMenus = (location: Location) => {
location.pathname.includes("/placement-groups");
const openImages =
location.pathname.includes("/image-registries") ||
location.pathname.includes("/image-registry/") ||
location.pathname.includes("/local-images");

const initialOpenMenus: AccordionNavMenu[] = [];
Expand All @@ -72,38 +72,23 @@ const initialiseOpenNavMenus = (location: Location) => {
return initialOpenMenus;
};

const ALL_PROJECTS = "All projects";
export const ALL_PROJECTS = "All projects";

const initializeProjectName = (
isAllProjectsFromUrl: boolean,
isLoading: boolean,
project: LxdProject | undefined,
) => {
if (isAllProjectsFromUrl) {
return ALL_PROJECTS;
}

if (project && !isLoading) {
return project.name;
}
interface Props {
projectName: string;
setProjectName: (projectName: string) => void;
}

return "default";
};

const Navigation: FC = () => {
const Navigation: FC<Props> = ({ projectName, setProjectName }) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to consider the image registry detail page to open the images sub-menu and to mark it active on collapsing the images section (like on the local images or registry list page).

const { isRestricted, authMethod, isAuthenticated } = useAuth();
const { menuCollapsed, updateMenuCollapsed } = useMenuCollapsed();
const {
project,
isAllProjects: isAllProjectsFromUrl,
canViewProject,
isLoading,
} = useCurrentProject();

const isSmallScreen = useIsScreenBelow();
const [projectName, setProjectName] = useState(
initializeProjectName(isAllProjectsFromUrl, isLoading, project),
);
const isAllProjects = projectName === ALL_PROJECTS;
const { hasCustomVolumeIso, hasAccessManagement, hasImageRegistries } =
useSupportedFeatures();
Expand Down Expand Up @@ -493,6 +478,7 @@ const Navigation: FC = () => {
<NavLink
to={`${ROOT_PATH}/ui/image-registries`}
title={getNavTitle("image registries")}
activeUrlMatches={["/image-registry/"]}
onClick={softToggleMenu}
>
Image registries
Expand Down
2 changes: 2 additions & 0 deletions src/components/ResourceIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ResourceIconType =
| "volume"
| "iso-volume"
| "image"
| "image-registry"
| "metric"
| "oidc-identity"
| "placement-group"
Expand Down Expand Up @@ -48,6 +49,7 @@ const resourceIcons: Record<ResourceIconType, string> = {
volume: "storage-volume",
"iso-volume": "iso",
image: "image",
"image-registry": "image",
"oidc-identity": "user",
certificate: "certificate",
"auth-group": "user-group",
Expand Down
14 changes: 13 additions & 1 deletion src/context/useImageRegistries.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { useAuth } from "./auth";
import { fetchImageRegistries, fetchImageRegistry } from "api/image-registries";
import {
fetchImageRegistries,
fetchImageRegistry,
fetchRegistryImages,
} from "api/image-registries";

export const useImageRegistries = (isEnabled = true) => {
const { isFineGrained } = useAuth();
Expand All @@ -20,3 +24,11 @@ export const useImageRegistry = (name: string, isEnabled = true) => {
enabled: isEnabled && isFineGrained !== null,
});
};

export const useRegistryImages = (imageRegistryName: string) => {
return useQuery({
queryKey: [queryKeys.imageRegistries, imageRegistryName, "images"],
queryFn: async () => fetchRegistryImages(imageRegistryName),
enabled: !!imageRegistryName,
});
};
11 changes: 9 additions & 2 deletions src/pages/images/ImageRegistriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import PageHeader from "components/PageHeader";
import ImageRegistriesSearchFilter, {
type ImageRegistryFilter,
} from "./ImageRegistriesSearchFilter";
import { useSearchParams } from "react-router-dom";
import { Link, useSearchParams } from "react-router-dom";
import type { LxdImageRegistryProtocol } from "types/image";
import { isRegistryPublic } from "util/image-registries";
import { CreateImageRegistryButton } from "./actions/CreateImageRegistryButton";
import { ROOT_PATH } from "util/rootPath";

const ImageRegistriesList: FC = () => {
const notify = useNotify();
Expand Down Expand Up @@ -78,7 +79,13 @@ const ImageRegistriesList: FC = () => {
name: registry.name,
columns: [
{
content: registry.name,
content: (
<Link
to={`${ROOT_PATH}/ui/image-registry/${encodeURIComponent(registry.name)}`}
>
{registry.name}
</Link>
),
role: "rowheader",
"aria-label": "Name",
title: `Image registry ${registry.name}`,
Expand Down
79 changes: 79 additions & 0 deletions src/pages/images/ImageRegistryDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { FC } from "react";
import { useParams } from "react-router-dom";
import { CustomLayout, Row, Spinner } from "@canonical/react-components";
import type { TabLink } from "@canonical/react-components/dist/components/Tabs/Tabs";
import NotFound from "components/NotFound";
import TabLinks from "components/TabLinks";
import { useImageRegistry } from "context/useImageRegistries";
import ImageRegistryDetailHeader from "pages/images/ImageRegistryDetailHeader";
import ImageRegistryImages from "pages/images/ImageRegistryImages";
import ImageRegistryOverview from "pages/images/ImageRegistryOverview";
import { ROOT_PATH } from "util/rootPath";

interface Props {
projectName: string;
}

const ImageRegistryDetail: FC<Props> = ({ projectName }) => {
const { name, activeTab } = useParams<{
name: string;
activeTab?: string;
}>();

if (!name) {
return <>Missing name</>;
}

const { data: imageRegistry, error, isLoading } = useImageRegistry(name);

if (isLoading) {
return <Spinner className="u-loader" text="Loading..." isMainComponent />;
} else if (!imageRegistry) {
return (
<NotFound
entityType="image-registry"
entityName={name}
errorMessage={error?.message}
/>
);
}

const tabs: (string | TabLink)[] = ["Overview", "Images"];

return (
<CustomLayout
header={
<ImageRegistryDetailHeader
imageRegistry={imageRegistry}
project={projectName}
/>
}
contentClassName="detail-page"
>
<Row>
<TabLinks
tabs={tabs}
activeTab={activeTab}
tabUrl={`${ROOT_PATH}/ui/image-registry/${encodeURIComponent(name)}`}
/>

{!activeTab && (
<div role="tabpanel" aria-labelledby="overview">
<ImageRegistryOverview imageRegistry={imageRegistry} />
</div>
)}

{activeTab === "images" && (
<div role="tabpanel" aria-labelledby="images">
<ImageRegistryImages
imageRegistry={imageRegistry}
project={projectName}
/>
</div>
)}
</Row>
</CustomLayout>
);
};

export default ImageRegistryDetail;
Loading
Loading