diff --git a/frontend/Routes.ts b/frontend/Routes.ts
index b36f105fa5..eb7c694368 100644
--- a/frontend/Routes.ts
+++ b/frontend/Routes.ts
@@ -212,7 +212,7 @@ export const RouteObj = {
NamespaceDetails: `${hubRoutePrefix}/namespaces/:id`,
CreateNamespace: `${hubRoutePrefix}/namespaces/create`,
EditNamespace: `${hubRoutePrefix}/namespaces/:id/edit`,
-
+ EditNamespaceMetadataDetails: `${hubRoutePrefix}/namespaces/:id/edit-details`,
Approvals: `${hubRoutePrefix}/approvals`,
ApprovalDetails: `${hubRoutePrefix}/approvals/details/:id`,
diff --git a/frontend/hub/HubRouter.tsx b/frontend/hub/HubRouter.tsx
index cc96f69e16..e42a63b80a 100644
--- a/frontend/hub/HubRouter.tsx
+++ b/frontend/hub/HubRouter.tsx
@@ -9,6 +9,7 @@ import { HubDashboard } from './dashboard/Dashboard';
import { ExecutionEnvironments } from './execution-environments/ExecutionEnvironments';
import { NamespaceDetails } from './namespaces/HubNamespaceDetails';
import { CreateHubNamespace, EditHubNamespace } from './namespaces/HubNamespaceForm';
+import { EditHubNamespaceMetadata } from './namespaces/HubNamespaceMetadataForm';
import { CreateRemote } from './remotes/RemoteForm';
import { Namespaces } from './namespaces/HubNamespaces';
import { RemoteRegistries } from './remote-registries/RemoteRegistries';
@@ -34,6 +35,10 @@ export function HubRouter() {
} />
} />
} />
+ }
+ />
} />
} />
diff --git a/frontend/hub/api.tsx b/frontend/hub/api.tsx
new file mode 100644
index 0000000000..634021e6da
--- /dev/null
+++ b/frontend/hub/api.tsx
@@ -0,0 +1,103 @@
+import { AutomationServerType } from '../automation-servers/AutomationServer';
+import { activeAutomationServer } from '../automation-servers/AutomationServersProvider';
+
+function apiTag(strings: TemplateStringsArray, ...values: string[]) {
+ if (strings[0]?.[0] !== '/') {
+ throw new Error('Invalid URL');
+ }
+
+ let url = '';
+ strings.forEach((fragment, index) => {
+ url += fragment;
+ if (index !== strings.length - 1) {
+ url += encodeURIComponent(`${values.shift() ?? ''}`);
+ }
+ });
+
+ return url;
+}
+
+export function hubAPI(strings: TemplateStringsArray, ...values: string[]) {
+ let base = process.env.HUB_API_BASE_PATH;
+ if (!base) {
+ if (activeAutomationServer?.type === AutomationServerType.Galaxy) {
+ base = '/api/galaxy';
+ } else {
+ base = '/api/automation-hub';
+ }
+ }
+ return base + apiTag(strings, ...values);
+}
+
+export function pulpAPI(strings: TemplateStringsArray, ...values: string[]) {
+ let base = process.env.HUB_API_BASE_PATH;
+ if (!base) {
+ if (activeAutomationServer?.type === AutomationServerType.Galaxy) {
+ base = '/api/galaxy';
+ } else {
+ base = '/api/automation-hub';
+ }
+ }
+ return base + '/pulp/api/v3' + apiTag(strings, ...values);
+}
+
+export type QueryParams = {
+ [key: string]: string | undefined;
+};
+
+export function getQueryString(queryParams: QueryParams) {
+ return Object.entries(queryParams)
+ .map(([key, value = '']) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
+ .join('&');
+}
+
+const UUIDRegEx = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/i;
+
+export function parsePulpIDFromURL(url: string): string | null {
+ for (const section of url.split('/')) {
+ if (section.match(UUIDRegEx)) {
+ return section;
+ }
+ }
+
+ return null;
+}
+
+// pulp next links currently include full url - with the wrong server
+// "http://localhost:5001/api/page/next?what#ever" -> "/api/page/next?what#ever"
+// also has to handle hub links (starting with /api/) and undefined
+export function serverlessURL(url?: string) {
+ if (!url || url.startsWith('/')) {
+ return url;
+ }
+
+ const { pathname, search, hash } = new URL(url);
+ return `${pathname}${search}${hash}`;
+}
+
+export function pulpIdKeyFn(item: { pulp_id: string }) {
+ return item.pulp_id;
+}
+
+export function pulpHrefKeyFn(item: { pulp_href: string }) {
+ return item.pulp_href;
+}
+
+export function nameKeyFn(item: { name: string }) {
+ return item.name;
+}
+
+export function idKeyFn(item: { id: number | string }) {
+ return item.id;
+}
+
+export function collectionKeyFn(item: {
+ collection_version: { pulp_href: string };
+ repository: { name: string };
+}) {
+ return item.collection_version.pulp_href + '_' + item.repository.name;
+}
+
+export function appendTrailingSlash(url: string) {
+ return url.endsWith('/') ? url : url + '/';
+}
diff --git a/frontend/hub/collections/CollectionDetails.tsx b/frontend/hub/collections/CollectionDetails.tsx
index 5eb6ba7d5c..12a5b17cc2 100644
--- a/frontend/hub/collections/CollectionDetails.tsx
+++ b/frontend/hub/collections/CollectionDetails.tsx
@@ -52,7 +52,7 @@ import { StatusCell } from '../../common/Status';
import { useGet } from '../../common/crud/useGet';
import { hubAPI } from '../api/utils';
import { HubItemsResponse } from '../useHubView';
-import { CollectionVersionSearch } from './Collection';
+import { Collection } from './Collection';
import { useCollectionActions } from './hooks/useCollectionActions';
import { useCollectionColumns } from './hooks/useCollectionColumns';
@@ -64,7 +64,7 @@ export function CollectionDetails() {
params.namespace || ''
}&repository=${params.repository || ''} }`
);
- let collection: CollectionVersionSearch | undefined = undefined;
+ let collection: Collection | undefined = undefined;
if (data && data.data && data.data.length > 0) {
collection = data.data[0];
}
@@ -72,10 +72,10 @@ export function CollectionDetails() {
return (
@@ -109,7 +109,7 @@ export function CollectionDetails() {
);
}
-function CollectionDetailsTab(props: { collection?: CollectionVersionSearch }) {
+function CollectionDetailsTab(props: { collection?: Collection }) {
const { collection } = props;
const tableColumns = useCollectionColumns();
return ;
diff --git a/frontend/hub/collections/hooks/useCollectionColumns.tsx b/frontend/hub/collections/hooks/useCollectionColumns.tsx
index 11e8139301..e8d2da02bd 100644
--- a/frontend/hub/collections/hooks/useCollectionColumns.tsx
+++ b/frontend/hub/collections/hooks/useCollectionColumns.tsx
@@ -8,27 +8,19 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ITableColumn, TextCell } from '../../../../framework';
import { RouteObj } from '../../../Routes';
-import { CollectionVersionSearch } from '../Collection';
+import { Collection } from '../Collection';
export function useCollectionColumns(_options?: { disableSort?: boolean; disableLinks?: boolean }) {
const { t } = useTranslation();
- return useMemo[]>(
+ return useMemo[]>(
() => [
{
header: t('Name'),
- value: (collection) => collection.collection_version.name,
+ value: (collection) => collection.name,
cell: (collection) => (
),
card: 'name',
@@ -44,13 +36,12 @@ export function useCollectionColumns(_options?: { disableSort?: boolean; disable
{
header: t('Namespace'),
type: 'text',
- value: (collection) => collection.collection_version.namespace,
- sort: 'namespace',
+ value: (collection) => collection.namespace.name,
},
{
header: t('Description'),
type: 'description',
- value: (collection) => collection.collection_version.description,
+ value: (collection) => collection.latest_version.metadata.description,
card: 'description',
list: 'description',
},
@@ -58,19 +49,39 @@ export function useCollectionColumns(_options?: { disableSort?: boolean; disable
header: t('Modules'),
type: 'count',
value: (collection) =>
- collection.collection_version.contents.filter((c) => c.content_type === 'module').length,
+ collection.latest_version.metadata.contents.filter((c) => c.content_type === 'module')
+ .length,
+ },
+ {
+ header: t('Roles'),
+ type: 'count',
+ value: (collection) =>
+ collection.latest_version.metadata.contents.filter((c) => c.content_type === 'TODO')
+ .length,
+ },
+ {
+ header: t('Plugins'),
+ type: 'count',
+ value: (collection) =>
+ collection.latest_version.metadata.contents.filter((c) => c.content_type === 'TODO')
+ .length,
+ },
+ {
+ header: t('Dependencies'),
+ type: 'count',
+ value: (collection) => Object.keys(collection.latest_version.metadata.dependencies).length,
},
{
header: t('Updated'),
type: 'datetime',
- value: (collection) => collection.collection_version.pulp_created,
+ value: (collection) => collection.latest_version.created_at,
card: 'hidden',
list: 'secondary',
},
{
header: t('Version'),
type: 'text',
- value: (collection) => collection.collection_version.version,
+ value: (collection) => collection.latest_version.version,
card: 'hidden',
list: 'secondary',
sort: 'version',
@@ -78,14 +89,14 @@ export function useCollectionColumns(_options?: { disableSort?: boolean; disable
{
header: t('Signed state'),
cell: (collection) => {
- switch (collection.is_signed) {
- case true:
+ switch (collection.latest_version.sign_state) {
+ case 'signed':
return (
} variant="outline" color="green">
{t('Signed')}
);
- case false:
+ case 'unsigned':
return (
} variant="outline" color="orange">
{t('Unsigned')}
@@ -94,7 +105,12 @@ export function useCollectionColumns(_options?: { disableSort?: boolean; disable
}
},
list: 'secondary',
- value: (collection) => collection.is_signed,
+ value: (collection) => collection.latest_version.sign_state,
+ },
+ {
+ header: t('Tags'),
+ type: 'labels',
+ value: (collection) => collection.latest_version.metadata.tags.sort(),
},
],
[t]
diff --git a/frontend/hub/collections/hooks/useCollectionVersionColumns.tsx b/frontend/hub/collections/hooks/useCollectionVersionColumns.tsx
new file mode 100644
index 0000000000..11aef09556
--- /dev/null
+++ b/frontend/hub/collections/hooks/useCollectionVersionColumns.tsx
@@ -0,0 +1,91 @@
+import { Label } from '@patternfly/react-core';
+import {
+ AnsibleTowerIcon,
+ CheckCircleIcon,
+ ExclamationTriangleIcon,
+} from '@patternfly/react-icons';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ITableColumn, TextCell } from '../../../../framework';
+import { RouteObj } from '../../../Routes';
+import { CollectionVersionSearch } from '../CollectionVersionSearch';
+
+export function useCollectionVersionColumns() {
+ const { t } = useTranslation();
+ return useMemo[]>(
+ () => [
+ {
+ header: t('Name'),
+ value: (collection) => collection.collection_version.name,
+ cell: (collection) => (
+
+ ),
+ card: 'name',
+ list: 'name',
+ icon: () => ,
+ },
+ {
+ header: t('Repository'),
+ type: 'text',
+ value: (collection) => collection.repository.name,
+ },
+ {
+ header: t('Namespace'),
+ type: 'text',
+ value: (collection) => collection.collection_version.namespace,
+ },
+ {
+ header: t('Description'),
+ type: 'description',
+ value: (collection) => collection.collection_version.description,
+ card: 'description',
+ list: 'description',
+ },
+ {
+ header: t('Modules'),
+ type: 'count',
+ value: (collection) =>
+ collection.collection_version.contents.filter((c) => c.content_type === 'module').length,
+ },
+ {
+ header: t('Updated'),
+ type: 'datetime',
+ value: (collection) => collection.collection_version.pulp_created,
+ card: 'hidden',
+ list: 'secondary',
+ },
+ {
+ header: t('Version'),
+ type: 'text',
+ value: (collection) => collection.collection_version.version,
+ card: 'hidden',
+ list: 'secondary',
+ },
+ {
+ header: t('Signed state'),
+ cell: (collection) => {
+ switch (collection.is_signed) {
+ case true:
+ return (
+ } variant="outline" color="green">
+ {t('Signed')}
+
+ );
+ case false:
+ return (
+ } variant="outline" color="orange">
+ {t('Unsigned')}
+
+ );
+ }
+ },
+ list: 'secondary',
+ value: (collection) => collection.is_signed,
+ },
+ ],
+ [t]
+ );
+}
diff --git a/frontend/hub/namespaces/HubNamespace.tsx b/frontend/hub/namespaces/HubNamespace.tsx
index dc7d3f8152..2c4652e364 100644
--- a/frontend/hub/namespaces/HubNamespace.tsx
+++ b/frontend/hub/namespaces/HubNamespace.tsx
@@ -1,17 +1,29 @@
-export interface HubNamespace {
+export interface LinksType {
+ name: string;
+ url: string;
+}
+
+export interface LatestMetadataType {
pulp_href: string;
- id: number;
name: string;
company: string;
email: string;
- avatar_url: string;
description: string;
- groups: [
- {
- id: number;
- name: string;
- object_roles: string[];
- },
- ];
- related_fields: object;
+ resources: string;
+ links: LinksType[];
+ avatar_sha256: string | null;
+ avatar_url: string | null;
+ metadata_sha256: string;
+ task: string | null;
+ groups: string[];
+}
+
+export interface HubNamespace {
+ description?: string;
+ company?: string;
+ pulp_href: string;
+ pulp_created: string;
+ name: string;
+ my_permissions: string[];
+ latest_metadata: LatestMetadataType;
}
diff --git a/frontend/hub/namespaces/HubNamespaceDetails.tsx b/frontend/hub/namespaces/HubNamespaceDetails.tsx
index f1bef88351..9ee9600d55 100644
--- a/frontend/hub/namespaces/HubNamespaceDetails.tsx
+++ b/frontend/hub/namespaces/HubNamespaceDetails.tsx
@@ -1,25 +1,40 @@
import { DropdownPosition } from '@patternfly/react-core';
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useTranslation } from 'react-i18next';
-import { useParams } from 'react-router-dom';
-import { PageActions, PageHeader, PageLayout, PageTab, PageTabs } from '../../../framework';
-import { PageDetailsFromColumns } from '../../../framework/PageDetails/PageDetailsFromColumns';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ PageActions,
+ PageHeader,
+ PageTable,
+ PageLayout,
+ PageTab,
+ PageTabs,
+} from '../../../framework';
import { RouteObj } from '../../Routes';
import { useGet } from '../../common/crud/useGet';
import { hubAPI } from '../api/utils';
-import { HubItemsResponse } from '../useHubView';
import { HubNamespace } from './HubNamespace';
+import { HubNamespaceMetadataType } from './HubNamespaceMetadataType';
+import { useHubView } from '../useHubView';
+import { usePulpSearchView } from '../usePulpSearchView';
+import { useHubNamespaceDetailsToolbarActions } from './hooks/useHubNamespaceDetailsToolbarActions';
import { useHubNamespaceActions } from './hooks/useHubNamespaceActions';
-import { useHubNamespacesColumns } from './hooks/useHubNamespacesColumns';
+import { useHubNamespaceMetadataActions } from './hooks/useHubNamespaceMetadataActions';
+import { useHubNamespaceMetadataColumns } from './hooks/useHubNamespaceMetadataColumns';
+import { useCollectionFilters } from '../collections/hooks/useCollectionFilters';
+import { useHubNamespaceDetailsFilters } from '../namespaces/hooks/useHubNamespaceDetailsFilters';
+import { useCollectionVersionColumns } from '../collections/hooks/useCollectionVersionColumns';
+import { CollectionVersionSearch } from '../collections/CollectionVersionSearch';
export function NamespaceDetails() {
const { t } = useTranslation();
const params = useParams<{ id: string }>();
- const { data } = useGet>(
- hubAPI`/_ui/v1/namespaces/?limit=1&name=${params.id ?? ''}`
+ const { data } = useGet>(
+ `/api/automation-hub/pulp/api/v3/pulp_ansible/namespaces/?limit=1&name=${params.id ?? ''}`
);
let namespace: HubNamespace | undefined = undefined;
- if (data && data.data && data.data.length > 0) {
- namespace = data.data[0];
+ if (data && data.results && data.count > 0) {
+ namespace = data.results[0];
}
const pageActions = useHubNamespaceActions();
@@ -27,10 +42,7 @@ export function NamespaceDetails() {
actions={pageActions}
@@ -40,7 +52,10 @@ export function NamespaceDetails() {
}
/>
-
+
+
+
+
@@ -49,7 +64,62 @@ export function NamespaceDetails() {
}
function NamespaceDetailsTab(props: { namespace?: HubNamespace }) {
- const { namespace } = props;
- const tableColumns = useHubNamespacesColumns();
- return ;
+ const { t } = useTranslation();
+ const toolbarFilters = useHubNamespaceDetailsFilters();
+ const toolbarActions = useHubNamespaceDetailsToolbarActions();
+ const tableColumns = useHubNamespaceMetadataColumns();
+ const rowActions = useHubNamespaceMetadataActions();
+ const view = usePulpSearchView({
+ url: hubAPI`/v3/plugin/ansible/search/namespace-metadata/`,
+ keyFn: (item) => item.metadata.pulp_href + ':' + item.repository.name,
+ tableColumns,
+ toolbarFilters,
+ sortKey: 'order_by',
+ queryParams: { name: props?.namespace?.name },
+ });
+ return (
+
+
+
+ );
+}
+
+function CollectionsTab(props: { namespace?: HubNamespace }) {
+ const { t } = useTranslation();
+ const toolbarFilters = useCollectionFilters();
+ const tableColumns = useCollectionVersionColumns();
+ const view = useHubView({
+ url: hubAPI`/v3/plugin/ansible/search/collection-versions/`,
+ keyFn: (item) => item.collection_version.pulp_href + ':' + item.repository.name,
+ tableColumns,
+ queryParams: { namespace: props?.namespace?.name },
+ });
+ const navigate = useNavigate();
+
+ return (
+
+
+ toolbarFilters={toolbarFilters}
+ tableColumns={tableColumns}
+ errorStateTitle={t('Error loading collections')}
+ emptyStateTitle={t('No collections yet')}
+ emptyStateDescription={t('To get started, upload a collection.')}
+ emptyStateButtonText={t('Upload collection')}
+ emptyStateButtonClick={() => navigate(RouteObj.UploadCollection)}
+ {...view}
+ defaultTableView="list"
+ defaultSubtitle={t('Collection')}
+ />
+
+ );
}
diff --git a/frontend/hub/namespaces/HubNamespaceForm.tsx b/frontend/hub/namespaces/HubNamespaceForm.tsx
index 35210a1262..b29fb9faad 100644
--- a/frontend/hub/namespaces/HubNamespaceForm.tsx
+++ b/frontend/hub/namespaces/HubNamespaceForm.tsx
@@ -11,15 +11,16 @@ import { RouteObj } from '../../Routes';
import { useGet } from '../../common/crud/useGet';
import { usePatchRequest } from '../../common/crud/usePatchRequest';
import { usePostRequest } from '../../common/crud/usePostRequest';
-import { hubAPI } from '../api/utils';
import { HubNamespace } from './HubNamespace';
+import { pulpAPI } from '../api/utils';
+import { ItemsResponse } from '../../common/crud/Data';
export function CreateHubNamespace() {
const { t } = useTranslation();
const navigate = useNavigate();
const postRequest = usePostRequest();
const onSubmit: PageFormSubmitHandler = async (namespace) => {
- const createdNamespace = await postRequest(hubAPI`/_ui/v1/namespaces/`, namespace);
+ const createdNamespace = await postRequest(pulpAPI`/pulp_ansible/namespaces/`, namespace);
navigate(RouteObj.NamespaceDetails.replace(':id', createdNamespace.name.toString()));
};
return (
@@ -35,9 +36,11 @@ export function CreateHubNamespace() {
submitText={t('Create namespace')}
onSubmit={onSubmit}
onCancel={() => navigate(-1)}
- defaultValue={{ groups: [] }}
+ defaultValue={{
+ latest_metadata: { groups: [] },
+ }}
>
-
+
);
@@ -48,19 +51,22 @@ export function EditHubNamespace() {
const navigate = useNavigate();
const params = useParams<{ id?: string }>();
const name = params.id;
- const { data: namespace } = useGet(hubAPI`/_ui/v1/namespaces/${name ?? ''}/`);
+ const { data: namespacesResponse } = useGet>(
+ pulpAPI`/pulp_ansible/namespaces/?name=${name ?? ''}`
+ );
const patchRequest = usePatchRequest();
const onSubmit: PageFormSubmitHandler = async (namespace) => {
- await patchRequest(hubAPI`/_ui/v1/namespaces/`, namespace);
+ await patchRequest(pulpAPI`/pulp_ansible/namespaces/`, namespace);
navigate(-1);
};
- if (!namespace) {
+ if (!namespacesResponse || namespacesResponse.results.length === 0) {
return (
@@ -73,6 +79,7 @@ export function EditHubNamespace() {
breadcrumbs={[
{ label: t('Namespaces'), to: RouteObj.Namespaces },
{ label: t('Edit Namespace') },
+ { label: params.id },
]}
/>
@@ -80,16 +87,17 @@ export function EditHubNamespace() {
submitText={t('Save namespace')}
onSubmit={onSubmit}
onCancel={() => navigate(-1)}
- defaultValue={namespace}
+ defaultValue={namespacesResponse.results[0]}
>
-
+
);
}
-function HubNamespaceInputs() {
+function HubNamespaceInputs(props: { isReadOnly?: boolean }) {
const { t } = useTranslation();
+ const { isReadOnly } = props;
return (
<>
@@ -97,21 +105,7 @@ function HubNamespaceInputs() {
label={t('Name')}
placeholder={t('Enter name')}
isRequired
- />
-
- name="description"
- label={t('Description')}
- placeholder={t('Enter description')}
- />
-
- name="company"
- label={t('Company')}
- placeholder={t('Enter company')}
- />
-
- name="avatar_url"
- label={t('Logo URL')}
- placeholder={t('Enter logo URL')}
+ isReadOnly={isReadOnly}
/>
>
);
diff --git a/frontend/hub/namespaces/HubNamespaceMetadataForm.tsx b/frontend/hub/namespaces/HubNamespaceMetadataForm.tsx
new file mode 100644
index 0000000000..1db3574ef9
--- /dev/null
+++ b/frontend/hub/namespaces/HubNamespaceMetadataForm.tsx
@@ -0,0 +1,94 @@
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useParams } from 'react-router-dom';
+import {
+ PageForm,
+ PageFormSubmitHandler,
+ PageFormTextInput,
+ PageHeader,
+ PageLayout,
+} from '../../../framework';
+import { RouteObj } from '../../Routes';
+import { useGet } from '../../common/crud/useGet';
+import { usePatchRequest } from '../../common/crud/usePatchRequest';
+import { HubNamespaceMetadataType } from './HubNamespaceMetadataType';
+import { hubAPI } from '../api';
+import { ItemsResponse } from '../../common/crud/Data';
+
+export function EditHubNamespaceMetadata() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const params = useParams<{ id?: string }>();
+ const name = params.id;
+ const { data: namespacesResponse } = useGet>(
+ hubAPI`/v3/plugin/ansible/search/namespace-metadata/?name=${name ?? ''}`
+ );
+ const patchRequest = usePatchRequest();
+ const onSubmit: PageFormSubmitHandler = async (namespace) => {
+ await patchRequest(hubAPI`/v3/plugin/ansible/search/namespace-metadata/`, namespace);
+ navigate(-1);
+ };
+ if (!namespacesResponse || namespacesResponse.results.length === 0) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+
+ submitText={t('Save namespace')}
+ onSubmit={onSubmit}
+ onCancel={() => navigate(-1)}
+ defaultValue={namespacesResponse.results[0]}
+ >
+
+
+
+ );
+}
+
+function HubNamespaceMetadataInputs() {
+ const { t } = useTranslation();
+ return (
+ <>
+
+ name="metadata.name"
+ label={t('Name')}
+ placeholder={t('Enter name')}
+ isRequired
+ isReadOnly
+ />
+
+ name="metadata.description"
+ label={t('Description')}
+ placeholder={t('Enter description')}
+ />
+
+ name="metadata.company"
+ label={t('Company')}
+ placeholder={t('Enter company')}
+ />
+
+ name="metadata.avatar_url"
+ label={t('Logo')}
+ placeholder={t('Enter logo url')}
+ />
+ >
+ );
+}
diff --git a/frontend/hub/namespaces/HubNamespaceMetadataType.tsx b/frontend/hub/namespaces/HubNamespaceMetadataType.tsx
new file mode 100644
index 0000000000..c83120eac9
--- /dev/null
+++ b/frontend/hub/namespaces/HubNamespaceMetadataType.tsx
@@ -0,0 +1,33 @@
+export interface Metadata {
+ pulp_href: string;
+ name: string;
+ company?: string;
+ email?: string;
+ description?: string;
+ resources?: string;
+ links: [];
+ avatar_sha256: null | string;
+ avatar_url: null | string;
+ metadata_sha256: string;
+ groups: [];
+ task: string | null;
+}
+
+export interface Repository {
+ pulp_href: string;
+ pulp_created: string;
+ versions_href: string;
+ pulp_labels: { pipeline: string };
+ latest_version_href: string;
+ name: string;
+ description: string;
+ retain_repo_versions: number;
+ remote: null | string;
+}
+
+export interface HubNamespaceMetadataType {
+ metadata: Metadata;
+ repository: Repository;
+ in_latest_repo_version: boolean;
+ in_old_repo_version: boolean;
+}
diff --git a/frontend/hub/namespaces/HubNamespaces.tsx b/frontend/hub/namespaces/HubNamespaces.tsx
index 05d1bbbb96..88503be640 100644
--- a/frontend/hub/namespaces/HubNamespaces.tsx
+++ b/frontend/hub/namespaces/HubNamespaces.tsx
@@ -2,17 +2,18 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { PageHeader, PageLayout, PageTab, PageTable, PageTabs } from '../../../framework';
import { RouteObj } from '../../Routes';
-import { useHubView } from '../useHubView';
+import { usePulpView } from '../usePulpView';
import { HubNamespace } from './HubNamespace';
import { useHubNamespaceActions } from './hooks/useHubNamespaceActions';
import { useHubNamespaceFilters } from './hooks/useHubNamespaceFilters';
import { useHubNamespaceToolbarActions } from './hooks/useHubNamespaceToolbarActions';
import { useHubNamespacesColumns } from './hooks/useHubNamespacesColumns';
-import { hubAPI } from '../api/utils';
-import { idKeyFn } from '../../common/utils/nameKeyFn';
+import { nameKeyFn } from '../../common/utils/nameKeyFn';
+import { pulpAPI } from '../api/utils';
export function Namespaces() {
const { t } = useTranslation();
+
return (
;
+ return ;
}
export function MyNamespaces() {
- return ;
+ return (
+
+ );
}
-export function CommonNamespaces({ url }: { url: string }) {
+export function CommonNamespaces({
+ url,
+ queryParams,
+}: {
+ url: string;
+ queryParams:
+ | {
+ [key: string]: string;
+ }
+ | undefined;
+}) {
const { t } = useTranslation();
const navigate = useNavigate();
const toolbarFilters = useHubNamespaceFilters();
const tableColumns = useHubNamespacesColumns();
const toolbarActions = useHubNamespaceToolbarActions();
const rowActions = useHubNamespaceActions();
- const view = useHubView({ url, keyFn: idKeyFn, toolbarFilters, tableColumns });
+ const view = usePulpView({
+ url,
+ keyFn: nameKeyFn,
+ toolbarFilters,
+ tableColumns,
+ queryParams,
+ });
return (
toolbarFilters={toolbarFilters}
diff --git a/frontend/hub/namespaces/hooks/useDeleteHubNamespaces.tsx b/frontend/hub/namespaces/hooks/useDeleteHubNamespaces.tsx
index 1598d34ce7..86b9f8483b 100644
--- a/frontend/hub/namespaces/hooks/useDeleteHubNamespaces.tsx
+++ b/frontend/hub/namespaces/hooks/useDeleteHubNamespaces.tsx
@@ -3,7 +3,7 @@ import { compareStrings, useBulkConfirmation } from '../../../../framework';
import { requestDelete } from '../../../common/crud/Data';
import { HubNamespace } from '../HubNamespace';
import { useHubNamespacesColumns } from './useHubNamespacesColumns';
-import { hubAPI } from '../../api/utils';
+import { pulpAPI } from '../../api/utils';
import { nameKeyFn } from '../../../common/utils/nameKeyFn';
export function useDeleteHubNamespaces(onComplete: (namespaces: HubNamespace[]) => void) {
@@ -26,7 +26,7 @@ export function useDeleteHubNamespaces(onComplete: (namespaces: HubNamespace[])
onComplete,
alertPrompts: [t('Deleting a namespace will delete all collections in the namespace.')],
actionFn: (namespace: HubNamespace) =>
- requestDelete(hubAPI`/_ui/v1/namespaces/${namespace.name}/`),
+ requestDelete(pulpAPI`/pulp_ansible/namespaces/${namespace.name}`),
});
};
return deleteHubNamespaces;
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaceDetailsFilters.tsx b/frontend/hub/namespaces/hooks/useHubNamespaceDetailsFilters.tsx
new file mode 100644
index 0000000000..8f834b7cb1
--- /dev/null
+++ b/frontend/hub/namespaces/hooks/useHubNamespaceDetailsFilters.tsx
@@ -0,0 +1,20 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { IToolbarFilter, ToolbarFilterType } from '../../../../framework';
+
+export function useHubNamespaceDetailsFilters() {
+ const { t } = useTranslation();
+ const toolbarFilters = useMemo(
+ () => [
+ {
+ key: 'repository_name__icontains',
+ label: t('Repository'),
+ type: ToolbarFilterType.Text,
+ query: 'repository_name__icontains',
+ comparison: 'contains',
+ },
+ ],
+ [t]
+ );
+ return toolbarFilters;
+}
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaceDetailsToolbarActions.tsx b/frontend/hub/namespaces/hooks/useHubNamespaceDetailsToolbarActions.tsx
new file mode 100644
index 0000000000..ee15a96a2e
--- /dev/null
+++ b/frontend/hub/namespaces/hooks/useHubNamespaceDetailsToolbarActions.tsx
@@ -0,0 +1,25 @@
+import { TrashIcon } from '@patternfly/react-icons';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { IPageAction, PageActionSelection, PageActionType } from '../../../../framework';
+import { HubNamespaceMetadataType } from '../HubNamespaceMetadataType';
+import { useDeleteHubNamespaces } from './useDeleteHubNamespaces';
+
+export function useHubNamespaceDetailsToolbarActions() {
+ const { t } = useTranslation();
+ const deleteHubNamespaces = useDeleteHubNamespaces(() => null);
+
+ return useMemo[]>(
+ () => [
+ {
+ type: PageActionType.Button,
+ selection: PageActionSelection.Multiple,
+ icon: TrashIcon,
+ label: t('Delete selected namesapces'),
+ onClick: deleteHubNamespaces,
+ isDanger: true,
+ },
+ ],
+ [deleteHubNamespaces, t]
+ );
+}
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaceFilters.tsx b/frontend/hub/namespaces/hooks/useHubNamespaceFilters.tsx
index 6ed6c1be6e..8c6d594e14 100644
--- a/frontend/hub/namespaces/hooks/useHubNamespaceFilters.tsx
+++ b/frontend/hub/namespaces/hooks/useHubNamespaceFilters.tsx
@@ -7,10 +7,10 @@ export function useHubNamespaceFilters() {
const toolbarFilters = useMemo(
() => [
{
- key: 'keywords',
+ key: 'name__icontains',
label: t('Name'),
type: ToolbarFilterType.Text,
- query: 'keywords',
+ query: 'name__icontains',
comparison: 'contains',
},
],
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaceMetadataActions.tsx b/frontend/hub/namespaces/hooks/useHubNamespaceMetadataActions.tsx
new file mode 100644
index 0000000000..6a2c5ada2c
--- /dev/null
+++ b/frontend/hub/namespaces/hooks/useHubNamespaceMetadataActions.tsx
@@ -0,0 +1,40 @@
+import { ButtonVariant } from '@patternfly/react-core';
+import { EditIcon, TrashIcon } from '@patternfly/react-icons';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { IPageAction, PageActionSelection, PageActionType } from '../../../../framework';
+import { RouteObj } from '../../../Routes';
+import { HubNamespaceMetadataType } from '../HubNamespaceMetadataType';
+import { useDeleteHubNamespaces } from './useDeleteHubNamespaces';
+
+export function useHubNamespaceMetadataActions() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const deleteHubNamespaces = useDeleteHubNamespaces(() => null);
+ return useMemo(() => {
+ const actions: IPageAction[] = [
+ {
+ type: PageActionType.Button,
+ selection: PageActionSelection.Single,
+ variant: ButtonVariant.primary,
+ isPinned: true,
+ icon: EditIcon,
+ label: t('Edit namespace details'),
+ onClick: (namespace) => {
+ navigate(RouteObj.EditNamespaceDetails.replace(':id', namespace.metadata.name));
+ },
+ },
+ { type: PageActionType.Seperator },
+ {
+ type: PageActionType.Button,
+ selection: PageActionSelection.Single,
+ icon: TrashIcon,
+ label: t('Delete namespace'),
+ onClick: (namespace) => deleteHubNamespaces([namespace]),
+ isDanger: true,
+ },
+ ];
+ return actions;
+ }, [deleteHubNamespaces, navigate, t]);
+}
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaceMetadataColumns.tsx b/frontend/hub/namespaces/hooks/useHubNamespaceMetadataColumns.tsx
new file mode 100644
index 0000000000..ce375da5c2
--- /dev/null
+++ b/frontend/hub/namespaces/hooks/useHubNamespaceMetadataColumns.tsx
@@ -0,0 +1,51 @@
+import { RedhatIcon } from '@patternfly/react-icons';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ITableColumn, TextCell } from '../../../../framework';
+// routeobj will be used to direct to the edit ns metadata form.
+// import { RouteObj } from '../../../Routes';
+import { HubNamespaceMetadataType } from '../HubNamespaceMetadataType';
+export function useHubNamespaceMetadataColumns(_options?: {
+ disableSort?: boolean;
+ disableLinks?: boolean;
+}) {
+ const { t } = useTranslation();
+ const tableColumns = useMemo[]>(
+ () => [
+ {
+ header: t('Name'),
+ cell: (namespace) => (
+
+ ),
+ value: (namespace) => namespace.metadata.name,
+ sort: 'name',
+ card: 'name',
+ list: 'name',
+ icon: () => ,
+ },
+ {
+ header: t('Description'),
+ type: 'description',
+ value: (namespace) => namespace.metadata.description ?? undefined,
+ card: 'description',
+ list: 'description',
+ },
+ {
+ header: t('Repository'),
+ type: 'text',
+ value: (namespace) => namespace.repository.name ?? undefined,
+ },
+ {
+ header: t('Links'),
+ type: 'text',
+ value: (namespace) => namespace.metadata.links ?? undefined,
+ },
+ ],
+ [t]
+ );
+ return tableColumns;
+}
diff --git a/frontend/hub/namespaces/hooks/useHubNamespaces.tsx b/frontend/hub/namespaces/hooks/useHubNamespaces.tsx
index d50f5ec6a5..8acbe279e6 100644
--- a/frontend/hub/namespaces/hooks/useHubNamespaces.tsx
+++ b/frontend/hub/namespaces/hooks/useHubNamespaces.tsx
@@ -1,9 +1,9 @@
import { useGet } from '../../../common/crud/useGet';
-import { hubAPI } from '../../api/utils';
import { HubItemsResponse } from '../../useHubView';
import { HubNamespace } from '../HubNamespace';
+import { pulpAPI } from '../../api/utils';
export function useHubNamespaces() {
- const t = useGet>(hubAPI`/_ui/v1/namespaces/`);
- return t.data?.data;
+ const t = useGet>(pulpAPI`/pulp_ansible/namespaces/`);
+ return t.data?.results;
}
diff --git a/frontend/hub/useHubView.tsx b/frontend/hub/useHubView.tsx
index c6f792b071..c3dc078a92 100644
--- a/frontend/hub/useHubView.tsx
+++ b/frontend/hub/useHubView.tsx
@@ -22,6 +22,18 @@ export interface HubItemsResponse {
};
}
+export interface HubItemsResponse2 {
+ count: number;
+ next?: string | null;
+ previous?: string | null;
+ results: T[];
+}
+
+export interface HubNamespaceResponse {
+ count: number;
+ results: T[];
+}
+
export type IHubView = IView &
ISelected & {
itemCount: number | undefined;
@@ -148,22 +160,3 @@ export function useHubView({
};
}, [data?.data, error, refresh, selection, unselectItemsAndRefresh, view]);
}
-
-export async function getAwxError(err: unknown) {
- if (err instanceof HTTPError) {
- try {
- const response = (await err.response.json()) as { __all__?: string[] };
- if ('__all__' in response && Array.isArray(response.__all__)) {
- return JSON.stringify(response.__all__[0]);
- } else {
- return JSON.stringify(response);
- }
- } catch {
- return err.message;
- }
- } else if (err instanceof Error) {
- return err.message;
- } else {
- return 'unknown error';
- }
-}
diff --git a/frontend/hub/usePulpSearchView.tsx b/frontend/hub/usePulpSearchView.tsx
new file mode 100644
index 0000000000..dd5b6e5162
--- /dev/null
+++ b/frontend/hub/usePulpSearchView.tsx
@@ -0,0 +1,145 @@
+import { HTTPError } from 'ky';
+import { useCallback, useMemo, useRef } from 'react';
+import useSWR from 'swr';
+import {
+ ISelected,
+ ITableColumn,
+ IToolbarFilter,
+ IView,
+ useSelected,
+ useView,
+} from '../../framework';
+import { useFetcher } from '../common/crud/Data';
+import { QueryParams, getQueryString, serverlessURL } from './api';
+
+export interface PulpSearchItemsResponse {
+ count: number;
+ next?: string | null;
+ previous?: string | null;
+ results: T[];
+}
+
+export type IHubView = IView &
+ ISelected & {
+ itemCount: number | undefined;
+ pageItems: T[] | undefined;
+ refresh: () => Promise | undefined>;
+ unselectItemsAndRefresh: (items: T[]) => void;
+ };
+
+export function usePulpSearchView({
+ url,
+ keyFn,
+ toolbarFilters,
+ tableColumns,
+ disableQueryString,
+ queryParams,
+ sortKey,
+ defaultFilters,
+}: {
+ url: string;
+ keyFn: (item: T) => string | number;
+ toolbarFilters?: IToolbarFilter[];
+ tableColumns?: ITableColumn[];
+ disableQueryString?: boolean;
+ queryParams?: QueryParams;
+ sortKey?: string;
+ defaultFilters?: Record;
+}): IHubView {
+ const view = useView({
+ defaultValues: {
+ sort: tableColumns && tableColumns.length ? tableColumns[0].sort : undefined,
+ filterState: defaultFilters,
+ },
+ disableQueryString,
+ });
+ const itemCountRef = useRef<{ itemCount: number | undefined }>({ itemCount: undefined });
+
+ const { page, perPage, sort, sortDirection, filterState } = view;
+
+ let queryString = queryParams ? `?${getQueryString(queryParams)}` : '';
+
+ if (filterState) {
+ for (const key in filterState) {
+ const toolbarFilter = toolbarFilters?.find((filter) => filter.key === key);
+ if (toolbarFilter) {
+ const values = filterState[key];
+ if (values.length > 0) {
+ queryString ? (queryString += '&') : (queryString += '?');
+ if (values.length > 1) {
+ queryString += values.map((value) => `or__${toolbarFilter.query}=${value}`).join('&');
+ } else {
+ queryString += `${toolbarFilter.query}=${values.join(',')}`;
+ }
+ }
+ }
+ }
+ }
+
+ if (sort) {
+ if (!sortKey) {
+ sortKey = 'sort';
+ }
+ queryString ? (queryString += '&') : (queryString += '?');
+ if (sortDirection === 'desc') {
+ queryString += `${sortKey}=-${sort}`;
+ } else {
+ queryString += `${sortKey}=${sort}`;
+ }
+ }
+
+ queryString ? (queryString += '&') : (queryString += '?');
+ queryString += `offset=${(page - 1) * perPage}`;
+
+ queryString ? (queryString += '&') : (queryString += '?');
+ queryString += `limit=${perPage}`;
+
+ url += queryString;
+ const fetcher = useFetcher();
+ const response = useSWR>(url, fetcher, {
+ dedupingInterval: 0,
+ refreshInterval: 30000,
+ });
+ const { data, mutate } = response;
+ const refresh = useCallback(() => mutate(), [mutate]);
+
+ const nextPage = serverlessURL(data?.next);
+ useSWR>(nextPage, fetcher, {
+ dedupingInterval: 0,
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ let error: Error | undefined = response.error;
+ if (error instanceof HTTPError) {
+ if (error.response.status === 404 && view.page > 1) {
+ view.setPage(1);
+ error = undefined;
+ }
+ }
+
+ const selection = useSelected(data?.results ?? [], keyFn);
+
+ if (data?.count !== undefined) {
+ itemCountRef.current.itemCount = data?.count;
+ }
+
+ const unselectItemsAndRefresh = useCallback(
+ (items: T[]) => {
+ selection.unselectItems(items);
+ void refresh();
+ },
+ [refresh, selection]
+ );
+
+ return useMemo(() => {
+ return {
+ refresh,
+ itemCount: itemCountRef.current.itemCount,
+ pageItems: data?.results,
+ error,
+ ...view,
+ ...selection,
+ unselectItemsAndRefresh,
+ };
+ }, [data?.results, error, refresh, selection, unselectItemsAndRefresh, view]);
+}