1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

refactor(namespace): migrate namespace edit to react [r8s-125] (#38)

This commit is contained in:
Ali 2024-12-11 10:15:46 +13:00 committed by GitHub
parent 40c7742e46
commit ce7e0d8d60
108 changed files with 3183 additions and 2194 deletions

View file

@ -1,7 +1,12 @@
import { compact } from 'lodash';
import { EnvironmentId } from '@/react/portainer/environments/types';
export const queryKeys = {
list: (environmentId: number, options?: { withResourceQuota?: boolean }) =>
list: (
environmentId: EnvironmentId,
options?: { withResourceQuota?: boolean }
) =>
compact([
'environments',
environmentId,
@ -9,7 +14,7 @@ export const queryKeys = {
'namespaces',
options?.withResourceQuota,
]),
namespace: (environmentId: number, namespace: string) =>
namespace: (environmentId: EnvironmentId, namespace: string) =>
[
'environments',
environmentId,
@ -17,4 +22,13 @@ export const queryKeys = {
'namespaces',
namespace,
] as const,
namespaceYAML: (environmentId: EnvironmentId, namespace: string) =>
[
'environments',
environmentId,
'kubernetes',
'namespaces',
namespace,
'yaml',
] as const,
};

View file

@ -0,0 +1,86 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
import { updateEnvironmentRegistryAccess } from '@/react/portainer/environments/environment.service/registries';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { IngressControllerClassMap } from '../../cluster/ingressClass/types';
import { updateIngressControllerClassMap } from '../../cluster/ingressClass/useIngressControllerClassMap';
import { Namespaces, NamespacePayload, UpdateRegistryPayload } from '../types';
import { queryKeys } from './queryKeys';
export function useCreateNamespaceMutation(environmentId: EnvironmentId) {
const queryClient = useQueryClient();
return useMutation(
async ({
createNamespacePayload,
updateRegistriesPayload,
namespaceIngressControllerPayload,
}: {
createNamespacePayload: NamespacePayload;
updateRegistriesPayload: UpdateRegistryPayload[];
namespaceIngressControllerPayload: IngressControllerClassMap[];
}) => {
try {
// create the namespace first, so that it exists before referencing it in the registry access request
await createNamespace(environmentId, createNamespacePayload);
} catch (e) {
throw new Error(e as string);
}
// collect promises
const updateRegistriesPromises = updateRegistriesPayload.map(
({ Id, Namespaces }) =>
updateEnvironmentRegistryAccess(environmentId, Id, {
Namespaces,
})
);
const updateIngressControllerPromise =
namespaceIngressControllerPayload.length > 0
? updateIngressControllerClassMap(
environmentId,
namespaceIngressControllerPayload,
createNamespacePayload.Name
)
: Promise.resolve();
// return combined promises
return Promise.allSettled([
updateIngressControllerPromise,
...updateRegistriesPromises,
]);
},
{
...withGlobalError('Unable to create namespace'),
...withInvalidate(queryClient, [queryKeys.list(environmentId)]),
}
);
}
// createNamespace is used to create a namespace using the Portainer backend
async function createNamespace(
environmentId: EnvironmentId,
payload: NamespacePayload
) {
try {
const { data: ns } = await axios.post<Namespaces>(
buildUrl(environmentId),
payload
);
return ns;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to create namespace');
}
}
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
let url = `kubernetes/${environmentId}/namespaces`;
if (namespace) {
url += `/${namespace}`;
}
return url;
}

View file

@ -4,10 +4,11 @@ import { PortainerNamespace } from '../types';
import { useNamespaceQuery } from './useNamespaceQuery';
export function useIsSystemNamespace(namespace: string) {
export function useIsSystemNamespace(namespace: string, enabled = true) {
const envId = useEnvironmentId();
const query = useNamespaceQuery(envId, namespace, {
select: (namespace) => namespace.IsSystem,
enabled,
});
return !!query.data;

View file

@ -8,19 +8,26 @@ import { PortainerNamespace } from '../types';
import { queryKeys } from './queryKeys';
type QueryParams = 'withResourceQuota';
export function useNamespaceQuery<T = PortainerNamespace>(
environmentId: EnvironmentId,
namespace: string,
{
select,
enabled,
params,
}: {
select?(namespace: PortainerNamespace): T;
params?: Record<QueryParams, string>;
enabled?: boolean;
} = {}
) {
return useQuery(
queryKeys.namespace(environmentId, namespace),
() => getNamespace(environmentId, namespace),
() => getNamespace(environmentId, namespace, params),
{
enabled: !!environmentId && !!namespace && enabled,
onError: (err) => {
notifyError('Failure', err as Error, 'Unable to get namespace.');
},
@ -32,11 +39,15 @@ export function useNamespaceQuery<T = PortainerNamespace>(
// getNamespace is used to retrieve a namespace using the Portainer backend
export async function getNamespace(
environmentId: EnvironmentId,
namespace: string
namespace: string,
params?: Record<QueryParams, string>
) {
try {
const { data: ns } = await axios.get<PortainerNamespace>(
`kubernetes/${environmentId}/namespaces/${namespace}`
`kubernetes/${environmentId}/namespaces/${namespace}`,
{
params,
}
);
return ns;
} catch (e) {

View file

@ -0,0 +1,71 @@
import { useQuery } from '@tanstack/react-query';
import axios from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { isFulfilled } from '@/portainer/helpers/promise-utils';
import { parseKubernetesAxiosError } from '../../axiosError';
import { generateResourceQuotaName } from '../resourceQuotaUtils';
import { queryKeys } from './queryKeys';
/**
* Gets the YAML for a namespace and its resource quota directly from the K8s proxy API.
*/
export function useNamespaceYAML(
environmentId: EnvironmentId,
namespaceName: string
) {
return useQuery({
queryKey: queryKeys.namespaceYAML(environmentId, namespaceName),
queryFn: () => composeNamespaceYAML(environmentId, namespaceName),
});
}
async function composeNamespaceYAML(
environmentId: EnvironmentId,
namespace: string
) {
const settledPromises = await Promise.allSettled([
getNamespaceYAML(environmentId, namespace),
getResourceQuotaYAML(environmentId, namespace),
]);
const resolvedPromises = settledPromises.filter(isFulfilled);
return resolvedPromises.map((p) => p.value).join('\n---\n');
}
async function getNamespaceYAML(
environmentId: EnvironmentId,
namespace: string
) {
try {
const { data: yaml } = await axios.get<string>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}`,
{
headers: {
Accept: 'application/yaml',
},
}
);
return yaml;
} catch (error) {
throw parseKubernetesAxiosError(error, 'Unable to retrieve namespace YAML');
}
}
async function getResourceQuotaYAML(
environmentId: EnvironmentId,
namespace: string
) {
const resourceQuotaName = generateResourceQuotaName(namespace);
try {
const { data: yaml } = await axios.get<string>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/resourcequotas/${resourceQuotaName}`,
{ headers: { Accept: 'application/yaml' } }
);
return yaml;
} catch (e) {
// silently ignore if resource quota does not exist
return null;
}
}

View file

@ -0,0 +1,37 @@
import { useQuery } from '@tanstack/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { notifyError } from '@/portainer/services/notifications';
import axios, { parseAxiosError } from '@/portainer/services/axios';
type K8sNodeLimits = {
CPU: number;
Memory: number;
};
/**
* useClusterResourceLimitsQuery is used to retrieve the total resource limits for a cluster, minus the allocated resources taken by existing namespaces
* @returns the available resource limits for the cluster
* */
export function useClusterResourceLimitsQuery(environmentId: EnvironmentId) {
return useQuery(
['environments', environmentId, 'kubernetes', 'max_resource_limits'],
() => getResourceLimits(environmentId),
{
onError: (err) => {
notifyError('Failure', err as Error, 'Unable to get resource limits');
},
}
);
}
async function getResourceLimits(environmentId: EnvironmentId) {
try {
const { data: limits } = await axios.get<K8sNodeLimits>(
`/kubernetes/${environmentId}/max_resource_limits`
);
return limits;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve resource limits');
}
}

View file

@ -0,0 +1,34 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
import { queryKeys } from './queryKeys';
export function useToggleSystemNamespaceMutation(
environmentId: EnvironmentId,
namespaceName: string
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (isSystem: boolean) =>
toggleSystemNamespace(environmentId, namespaceName, isSystem),
...withInvalidate(queryClient, [
queryKeys.namespace(environmentId, namespaceName),
]),
...withGlobalError('Failed to update namespace'),
});
}
async function toggleSystemNamespace(
environmentId: EnvironmentId,
namespaceName: string,
system: boolean
) {
const response = await axios.put(
`/kubernetes/${environmentId}/namespaces/${namespaceName}/system`,
{ system }
);
return response.data;
}

View file

@ -0,0 +1,83 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
import { updateEnvironmentRegistryAccess } from '@/react/portainer/environments/environment.service/registries';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { notifyError } from '@/portainer/services/notifications';
import { IngressControllerClassMap } from '../../cluster/ingressClass/types';
import { updateIngressControllerClassMap } from '../../cluster/ingressClass/useIngressControllerClassMap';
import { Namespaces, NamespacePayload, UpdateRegistryPayload } from '../types';
import { queryKeys } from './queryKeys';
export function useUpdateNamespaceMutation(environmentId: EnvironmentId) {
const queryClient = useQueryClient();
return useMutation(
async ({
createNamespacePayload,
updateRegistriesPayload,
namespaceIngressControllerPayload,
}: {
createNamespacePayload: NamespacePayload;
updateRegistriesPayload: UpdateRegistryPayload[];
namespaceIngressControllerPayload: IngressControllerClassMap[];
}) => {
const { Name: namespaceName } = createNamespacePayload;
const updatedNamespace = await updateNamespace(
environmentId,
namespaceName,
createNamespacePayload
);
// collect promises
const updateRegistriesPromises = updateRegistriesPayload.map(
({ Id, Namespaces }) =>
updateEnvironmentRegistryAccess(environmentId, Id, {
Namespaces,
})
);
const updateIngressControllerPromise = updateIngressControllerClassMap(
environmentId,
namespaceIngressControllerPayload,
createNamespacePayload.Name
);
const results = await Promise.allSettled([
updateIngressControllerPromise,
...updateRegistriesPromises,
]);
// Check for any failures in the additional updates
const failures = results.filter((result) => result.status === 'rejected');
failures.forEach((failure) => {
notifyError(
'Unable to update namespace',
undefined,
failure.reason as string
);
});
return updatedNamespace;
},
{
...withGlobalError('Unable to update namespace'),
...withInvalidate(queryClient, [queryKeys.list(environmentId)]),
}
);
}
// updateNamespace is used to update a namespace using the Portainer backend
async function updateNamespace(
environmentId: EnvironmentId,
namespace: string,
payload: NamespacePayload
) {
try {
const { data: ns } = await axios.put<Namespaces>(
`kubernetes/${environmentId}/namespaces/${namespace}`,
payload
);
return ns;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to create namespace');
}
}