mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 12:25:22 +02:00
refactor(k8s): namespace core logic (#12142)
Co-authored-by: testA113 <aliharriss1995@gmail.com> Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com> Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
parent
da010f3d08
commit
ea228c3d6d
276 changed files with 9241 additions and 3361 deletions
|
@ -1,12 +1,15 @@
|
|||
import { Layers } from 'lucide-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { pluralize } from '@/portainer/helpers/strings';
|
||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { refreshableSettings } from '@@/datatables/types';
|
||||
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||
import { useTableStateWithStorage } from '@@/datatables/useTableState';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { AddButton } from '@@/buttons';
|
||||
|
||||
import { systemResourcesSettings } from '../../datatables/SystemResourcesSettings';
|
||||
|
@ -17,19 +20,16 @@ import {
|
|||
} from '../../datatables/DefaultDatatableSettings';
|
||||
import { SystemResourceDescription } from '../../datatables/SystemResourceDescription';
|
||||
import { isDefaultNamespace } from '../isDefaultNamespace';
|
||||
import { useNamespacesQuery } from '../queries/useNamespacesQuery';
|
||||
import { PortainerNamespace } from '../types';
|
||||
import { useDeleteNamespaces } from '../queries/useDeleteNamespaces';
|
||||
import { queryKeys } from '../queries/queryKeys';
|
||||
|
||||
import { NamespaceViewModel } from './types';
|
||||
import { useColumns } from './columns/useColumns';
|
||||
|
||||
export function NamespacesDatatable({
|
||||
dataset,
|
||||
onRemove,
|
||||
onRefresh,
|
||||
}: {
|
||||
dataset: Array<NamespaceViewModel>;
|
||||
onRemove(items: Array<NamespaceViewModel>): void;
|
||||
onRefresh(): void;
|
||||
}) {
|
||||
export function NamespacesDatatable() {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const tableState = useTableStateWithStorage<TableSettings>(
|
||||
'kube-namespaces',
|
||||
'Name',
|
||||
|
@ -38,6 +38,11 @@ export function NamespacesDatatable({
|
|||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
const namespacesQuery = useNamespacesQuery(environmentId, {
|
||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
withResourceQuota: true,
|
||||
});
|
||||
const namespaces = Object.values(namespacesQuery.data ?? []);
|
||||
|
||||
const hasWriteAuthQuery = useAuthorizations(
|
||||
'K8sResourcePoolDetailsW',
|
||||
|
@ -45,40 +50,30 @@ export function NamespacesDatatable({
|
|||
true
|
||||
);
|
||||
const columns = useColumns();
|
||||
useRepeater(tableState.autoRefreshRate, onRefresh);
|
||||
|
||||
const filteredDataset = tableState.showSystemResources
|
||||
? dataset
|
||||
: dataset.filter((item) => !item.Namespace.IsSystem);
|
||||
? namespaces
|
||||
: namespaces.filter((namespace) => !namespace.IsSystem);
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
<Datatable<PortainerNamespace>
|
||||
data-cy="k8sNamespace-namespaceTable"
|
||||
dataset={filteredDataset}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
isLoading={namespacesQuery.isLoading}
|
||||
title="Namespaces"
|
||||
titleIcon={Layers}
|
||||
getRowId={(item) => item.Namespace.Id}
|
||||
getRowId={(item) => item.Id}
|
||||
disableSelect={!hasWriteAuthQuery.authorized}
|
||||
isRowSelectable={({ original: item }) =>
|
||||
hasWriteAuthQuery.authorized &&
|
||||
!item.Namespace.IsSystem &&
|
||||
!isDefaultNamespace(item.Namespace.Name)
|
||||
!item.IsSystem && !isDefaultNamespace(item.Name)
|
||||
}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<Authorized authorizations="K8sResourcePoolDetailsW" adminOnlyCE>
|
||||
<DeleteButton
|
||||
onClick={() => onRemove(selectedItems)}
|
||||
disabled={selectedItems.length === 0}
|
||||
data-cy="delete-namespace-button"
|
||||
/>
|
||||
|
||||
<AddButton color="secondary" data-cy="add-namespace-form-button">
|
||||
Add with form
|
||||
</AddButton>
|
||||
|
||||
<CreateFromManifestButton data-cy="k8s-namespaces-deploy-button" />
|
||||
</Authorized>
|
||||
<TableActions
|
||||
selectedItems={selectedItems}
|
||||
namespaces={namespacesQuery.data}
|
||||
/>
|
||||
)}
|
||||
renderTableSettings={() => (
|
||||
<TableSettingsMenu>
|
||||
|
@ -93,3 +88,101 @@ export function NamespacesDatatable({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableActions({
|
||||
selectedItems,
|
||||
namespaces: namespacesQueryData,
|
||||
}: {
|
||||
selectedItems: PortainerNamespace[];
|
||||
namespaces?: PortainerNamespace[];
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const environmentId = useEnvironmentId();
|
||||
const deleteNamespacesMutation = useDeleteNamespaces(environmentId);
|
||||
|
||||
const selectedNamespacePlural = pluralize(selectedItems.length, 'namespace');
|
||||
const includesTerminatingNamespace = selectedItems.some(
|
||||
(ns) => ns.Status.phase === 'Terminating'
|
||||
);
|
||||
const message = includesTerminatingNamespace
|
||||
? 'At least one namespace is in a terminating state. For terminating state namespaces, you may continue and force removal, but doing so without having properly cleaned up may lead to unstable and unpredictable behavior. Are you sure you wish to proceed?'
|
||||
: `Do you want to remove the selected ${selectedNamespacePlural}? All the resources associated to the selected ${selectedNamespacePlural} will be removed too. Are you sure you wish to proceed?`;
|
||||
|
||||
return (
|
||||
<Authorized authorizations="K8sResourcePoolDetailsW" adminOnlyCE>
|
||||
<DeleteButton
|
||||
onConfirmed={() => onRemoveNamespaces(selectedItems)}
|
||||
disabled={selectedItems.length === 0}
|
||||
data-cy="delete-namespace-button"
|
||||
confirmMessage={message}
|
||||
/>
|
||||
|
||||
<AddButton color="secondary" data-cy="add-namespace-form-button">
|
||||
Add with form
|
||||
</AddButton>
|
||||
|
||||
<CreateFromManifestButton data-cy="k8s-namespaces-deploy-button" />
|
||||
</Authorized>
|
||||
);
|
||||
|
||||
function onRemoveNamespaces(selectedNamespaces: Array<PortainerNamespace>) {
|
||||
deleteNamespacesMutation.mutate(
|
||||
{
|
||||
namespaceNames: selectedNamespaces.map((namespace) => namespace.Name),
|
||||
},
|
||||
{
|
||||
onSuccess: (resp) => {
|
||||
// gather errors and deleted namespaces
|
||||
const errors = resp.data?.errors || [];
|
||||
const erroredNamespacePlural = pluralize(errors.length, 'namespace');
|
||||
|
||||
const selectedNamespaceNames = selectedNamespaces.map(
|
||||
(ns) => ns.Name
|
||||
);
|
||||
const deletedNamespaces =
|
||||
resp.data?.deleted || selectedNamespaceNames;
|
||||
const deletedNamespacePlural = pluralize(
|
||||
deletedNamespaces.length,
|
||||
'namespace'
|
||||
);
|
||||
|
||||
// notify user of success and errors
|
||||
if (errors.length > 0) {
|
||||
notifyError(
|
||||
'Error',
|
||||
new Error(
|
||||
`Failed to delete ${erroredNamespacePlural}: ${errors
|
||||
.map((err) => `${err.namespaceName}: ${err.error}`)
|
||||
.join(', ')}`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (deletedNamespaces.length > 0) {
|
||||
notifySuccess(
|
||||
'Success',
|
||||
`Successfully deleted ${deletedNamespacePlural}: ${deletedNamespaces.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Plain invalidation / refetching is confusing because namespaces hang in a terminating state
|
||||
// instead, optimistically update the cache manually to hide the deleting (terminating) namespaces
|
||||
queryClient.setQueryData(
|
||||
queryKeys.list(environmentId, {
|
||||
withResourceQuota: true,
|
||||
}),
|
||||
() =>
|
||||
deletedNamespaces.reduce(
|
||||
(acc, ns) => {
|
||||
delete acc[ns as keyof typeof acc];
|
||||
return acc;
|
||||
},
|
||||
{ ...namespacesQueryData }
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
12
app/react/kubernetes/namespaces/ListView/NamespacesView.tsx
Normal file
12
app/react/kubernetes/namespaces/ListView/NamespacesView.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { NamespacesDatatable } from './NamespacesDatatable';
|
||||
|
||||
export function NamespacesView() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Namespace list" breadcrumbs="Namespaces" reload />
|
||||
<NamespacesDatatable />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -7,8 +7,8 @@ import { Environment } from '@/react/portainer/environments/types';
|
|||
import { Link } from '@@/Link';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { NamespaceViewModel } from '../types';
|
||||
import { isDefaultNamespace } from '../../isDefaultNamespace';
|
||||
import { PortainerNamespace } from '../../types';
|
||||
|
||||
import { helper } from './helper';
|
||||
|
||||
|
@ -18,15 +18,15 @@ export const actions = helper.display({
|
|||
});
|
||||
|
||||
function Cell({
|
||||
row: { original: item },
|
||||
}: CellContext<NamespaceViewModel, unknown>) {
|
||||
row: { original: namespace },
|
||||
}: CellContext<PortainerNamespace, unknown>) {
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
if (!environmentQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!canManageAccess(item, environmentQuery.data)) {
|
||||
if (!canManageAccess(namespace, environmentQuery.data)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
|
@ -36,22 +36,24 @@ function Cell({
|
|||
color="link"
|
||||
props={{
|
||||
to: 'kubernetes.resourcePools.resourcePool.access',
|
||||
params: { id: item.Namespace.Name },
|
||||
params: {
|
||||
id: namespace.Name,
|
||||
},
|
||||
}}
|
||||
icon={Users}
|
||||
data-cy={`manage-access-button-${item.Namespace.Name}`}
|
||||
data-cy={`manage-access-button-${namespace.Name}`}
|
||||
>
|
||||
Manage access
|
||||
</Button>
|
||||
);
|
||||
|
||||
function canManageAccess(item: NamespaceViewModel, environment: Environment) {
|
||||
const name = item.Namespace.Name;
|
||||
const isSystem = item.Namespace.IsSystem;
|
||||
|
||||
function canManageAccess(
|
||||
{ Name, IsSystem }: PortainerNamespace,
|
||||
environment: Environment
|
||||
) {
|
||||
return (
|
||||
!isSystem &&
|
||||
(!isDefaultNamespace(name) ||
|
||||
!IsSystem &&
|
||||
(!isDefaultNamespace(Name) ||
|
||||
environment.Kubernetes.Configuration.RestrictDefaultNamespace)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { NamespaceViewModel } from '../types';
|
||||
import { PortainerNamespace } from '../../types';
|
||||
|
||||
export const helper = createColumnHelper<NamespaceViewModel>();
|
||||
export const helper = createColumnHelper<PortainerNamespace>();
|
||||
|
|
|
@ -21,7 +21,7 @@ export function useColumns() {
|
|||
return useMemo(
|
||||
() =>
|
||||
_.compact([
|
||||
helper.accessor('Namespace.Name', {
|
||||
helper.accessor('Name', {
|
||||
header: 'Name',
|
||||
id: 'Name',
|
||||
cell: ({ getValue, row: { original: item } }) => {
|
||||
|
@ -38,7 +38,7 @@ export function useColumns() {
|
|||
>
|
||||
{name}
|
||||
</Link>
|
||||
{item.Namespace.IsSystem && (
|
||||
{item.IsSystem && (
|
||||
<span className="ml-2">
|
||||
<SystemBadge />
|
||||
</span>
|
||||
|
@ -47,14 +47,18 @@ export function useColumns() {
|
|||
);
|
||||
},
|
||||
}),
|
||||
helper.accessor('Namespace.Status', {
|
||||
helper.accessor('Status', {
|
||||
header: 'Status',
|
||||
cell({ getValue }) {
|
||||
const status = getValue();
|
||||
return <StatusBadge color={getColor(status)}>{status}</StatusBadge>;
|
||||
return (
|
||||
<StatusBadge color={getColor(status.phase)}>
|
||||
{status.phase}
|
||||
</StatusBadge>
|
||||
);
|
||||
|
||||
function getColor(status: string) {
|
||||
switch (status.toLowerCase()) {
|
||||
function getColor(status?: string) {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'success';
|
||||
case 'terminating':
|
||||
|
@ -65,7 +69,8 @@ export function useColumns() {
|
|||
}
|
||||
},
|
||||
}),
|
||||
helper.accessor('Quota', {
|
||||
helper.accessor('ResourceQuota', {
|
||||
header: 'Quota',
|
||||
cell({ getValue }) {
|
||||
const quota = getValue();
|
||||
|
||||
|
@ -76,15 +81,13 @@ export function useColumns() {
|
|||
return <Badge type="warn">Enabled</Badge>;
|
||||
},
|
||||
}),
|
||||
helper.accessor('Namespace.CreationDate', {
|
||||
helper.accessor('CreationDate', {
|
||||
header: 'Created',
|
||||
cell({ row: { original: item } }) {
|
||||
return (
|
||||
<>
|
||||
{isoDate(item.Namespace.CreationDate)}{' '}
|
||||
{item.Namespace.ResourcePoolOwner
|
||||
? ` by ${item.Namespace.ResourcePoolOwner}`
|
||||
: ''}
|
||||
{isoDate(item.CreationDate)}{' '}
|
||||
{item.NamespaceOwner ? ` by ${item.NamespaceOwner}` : ''}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
export interface NamespaceViewModel {
|
||||
Namespace: {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Status: string;
|
||||
CreationDate: number;
|
||||
ResourcePoolOwner: string;
|
||||
IsSystem: boolean;
|
||||
};
|
||||
Quota: number;
|
||||
}
|
|
@ -83,10 +83,10 @@ export function NamespaceInnerForm({
|
|||
onChange={(classes) => setFieldValue('ingressClasses', classes)}
|
||||
values={values.ingressClasses}
|
||||
description="Enable the ingress controllers that users can select when publishing applications in this namespace."
|
||||
noIngressControllerLabel="No ingress controllers available in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster."
|
||||
view="namespace"
|
||||
isLoading={ingressClassesQuery.isLoading}
|
||||
initialValues={initialValues.ingressClasses}
|
||||
noIngressControllerLabel="No ingress controllers available in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster."
|
||||
/>
|
||||
</FormSection>
|
||||
)}
|
||||
|
|
20
app/react/kubernetes/namespaces/queries/queryKeys.ts
Normal file
20
app/react/kubernetes/namespaces/queries/queryKeys.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { compact } from 'lodash';
|
||||
|
||||
export const queryKeys = {
|
||||
list: (environmentId: number, options?: { withResourceQuota?: boolean }) =>
|
||||
compact([
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'namespaces',
|
||||
options?.withResourceQuota,
|
||||
]),
|
||||
namespace: (environmentId: number, namespace: string) =>
|
||||
[
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'namespaces',
|
||||
namespace,
|
||||
] as const,
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
type DeleteNamespaceError = {
|
||||
namespaceName: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
// when successful, the response will contain a list of deleted namespaces and a list of errors
|
||||
type DeleteNamespacesResponse = {
|
||||
deleted: string[];
|
||||
errors: DeleteNamespaceError[];
|
||||
} | null;
|
||||
|
||||
// useDeleteNamespaces is a react query mutation that removes a list of namespaces,
|
||||
export function useDeleteNamespaces(environmentId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
({ namespaceNames }: { namespaceNames: string[] }) =>
|
||||
deleteNamespaces(environmentId, namespaceNames),
|
||||
{
|
||||
...withInvalidate(queryClient, [queryKeys.list(environmentId)]),
|
||||
...withGlobalError('Unable to delete namespaces'),
|
||||
// onSuccess handled by the caller
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteNamespaces(
|
||||
environmentId: number,
|
||||
namespaceNames: string[]
|
||||
) {
|
||||
try {
|
||||
return await axios.delete<DeleteNamespacesResponse>(
|
||||
`kubernetes/${environmentId}/namespaces`,
|
||||
{
|
||||
data: namespaceNames,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to delete namespace');
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { PortainerNamespace } from '../types';
|
||||
|
||||
import { useNamespaceQuery } from './useNamespaceQuery';
|
||||
|
||||
export function useIsSystemNamespace(namespace: string) {
|
||||
|
@ -10,3 +12,12 @@ export function useIsSystemNamespace(namespace: string) {
|
|||
|
||||
return !!query.data;
|
||||
}
|
||||
|
||||
export function isSystemNamespace(
|
||||
namespaceName: string,
|
||||
namespaces?: PortainerNamespace[]
|
||||
) {
|
||||
return namespaces?.some(
|
||||
(namespace) => namespace.Name === namespaceName && namespace.IsSystem
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,19 +4,21 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|||
import { notifyError } from '@/portainer/services/notifications';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
import { DefaultOrSystemNamespace } from '../types';
|
||||
import { PortainerNamespace } from '../types';
|
||||
|
||||
export function useNamespaceQuery<T = DefaultOrSystemNamespace>(
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export function useNamespaceQuery<T = PortainerNamespace>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
{
|
||||
select,
|
||||
}: {
|
||||
select?(namespace: DefaultOrSystemNamespace): T;
|
||||
select?(namespace: PortainerNamespace): T;
|
||||
} = {}
|
||||
) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'kubernetes', 'namespaces', namespace],
|
||||
queryKeys.namespace(environmentId, namespace),
|
||||
() => getNamespace(environmentId, namespace),
|
||||
{
|
||||
onError: (err) => {
|
||||
|
@ -33,11 +35,11 @@ export async function getNamespace(
|
|||
namespace: string
|
||||
) {
|
||||
try {
|
||||
const { data: ns } = await axios.get<DefaultOrSystemNamespace>(
|
||||
const { data: ns } = await axios.get<PortainerNamespace>(
|
||||
`kubernetes/${environmentId}/namespaces/${namespace}`
|
||||
);
|
||||
return ns;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve namespace');
|
||||
throw parseAxiosError(e, 'Unable to retrieve namespace');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,24 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { Namespaces } from '../types';
|
||||
import { getSelfSubjectAccessReview } from '../getSelfSubjectAccessReview';
|
||||
import { PortainerNamespace } from '../types';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export function useNamespacesQuery(
|
||||
environmentId: EnvironmentId,
|
||||
options?: { autoRefreshRate?: number }
|
||||
options?: { autoRefreshRate?: number; withResourceQuota?: boolean }
|
||||
) {
|
||||
return useQuery(
|
||||
['environments', environmentId, 'kubernetes', 'namespaces'],
|
||||
async () => {
|
||||
const namespaces = await getNamespaces(environmentId);
|
||||
const namespaceNames = Object.keys(namespaces);
|
||||
// use selfsubjectaccess reviews to avoid forbidden requests
|
||||
const allNamespaceAccessReviews = await Promise.all(
|
||||
namespaceNames.map((namespaceName) =>
|
||||
getSelfSubjectAccessReview(environmentId, namespaceName)
|
||||
)
|
||||
);
|
||||
const allowedNamespacesNames = allNamespaceAccessReviews
|
||||
.filter((accessReview) => accessReview.status.allowed)
|
||||
.map((accessReview) => accessReview.spec.resourceAttributes.namespace);
|
||||
const allowedNamespaces = namespaceNames.reduce((acc, namespaceName) => {
|
||||
if (allowedNamespacesNames.includes(namespaceName)) {
|
||||
acc[namespaceName] = namespaces[namespaceName];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Namespaces);
|
||||
return allowedNamespaces;
|
||||
},
|
||||
queryKeys.list(environmentId, {
|
||||
withResourceQuota: !!options?.withResourceQuota,
|
||||
}),
|
||||
async () => getNamespaces(environmentId, options?.withResourceQuota),
|
||||
{
|
||||
...withError('Unable to get namespaces.'),
|
||||
...withGlobalError('Unable to get namespaces.'),
|
||||
refetchInterval() {
|
||||
return options?.autoRefreshRate ?? false;
|
||||
},
|
||||
|
@ -43,13 +27,18 @@ export function useNamespacesQuery(
|
|||
}
|
||||
|
||||
// getNamespaces is used to retrieve namespaces using the Portainer backend with caching
|
||||
async function getNamespaces(environmentId: EnvironmentId) {
|
||||
export async function getNamespaces(
|
||||
environmentId: EnvironmentId,
|
||||
withResourceQuota?: boolean
|
||||
) {
|
||||
const params = withResourceQuota ? { withResourceQuota } : {};
|
||||
try {
|
||||
const { data: namespaces } = await axios.get<Namespaces>(
|
||||
`kubernetes/${environmentId}/namespaces`
|
||||
const { data: namespaces } = await axios.get<PortainerNamespace[]>(
|
||||
`kubernetes/${environmentId}/namespaces`,
|
||||
{ params }
|
||||
);
|
||||
return namespaces;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve namespaces');
|
||||
throw parseAxiosError(e, 'Unable to retrieve namespaces');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
export interface Namespaces {
|
||||
[key: string]: DefaultOrSystemNamespace;
|
||||
import { NamespaceStatus, ResourceQuota } from 'kubernetes-types/core/v1';
|
||||
|
||||
export interface PortainerNamespace {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Status: NamespaceStatus;
|
||||
CreationDate: number;
|
||||
NamespaceOwner: string;
|
||||
IsSystem: boolean;
|
||||
IsDefault: boolean;
|
||||
ResourceQuota?: ResourceQuota | null;
|
||||
}
|
||||
|
||||
export interface DefaultOrSystemNamespace {
|
||||
IsDefault: boolean;
|
||||
IsSystem: boolean;
|
||||
}
|
||||
// type returned via the internal portainer namespaces api, with simplified fields
|
||||
// it is a record currently (legacy reasons), but it should be an array
|
||||
export type Namespaces = Record<string, PortainerNamespace>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue