fix(namespace): create ns qa feedback [EE-2226] (#10474)

pull/10486/head
Ali 2023-10-16 19:15:44 +01:00 committed by GitHub
parent bcb3f918d1
commit 07ec2ffe5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 34 additions and 43 deletions

View File

@ -11,6 +11,7 @@ type K8sNamespaceDetails struct {
Name string `json:"Name"` Name string `json:"Name"`
Annotations map[string]string `json:"Annotations"` Annotations map[string]string `json:"Annotations"`
ResourceQuota *K8sResourceQuota `json:"ResourceQuota"` ResourceQuota *K8sResourceQuota `json:"ResourceQuota"`
Owner string `json:"Owner"`
} }
type K8sResourceQuota struct { type K8sResourceQuota struct {

View File

@ -63,15 +63,21 @@ func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, er
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint. // CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error { func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
portainerLabels := map[string]string{
"io.portainer.kubernetes.resourcepool.name": info.Name,
"io.portainer.kubernetes.resourcepool.owner": info.Owner,
}
var ns v1.Namespace var ns v1.Namespace
ns.Name = info.Name ns.Name = info.Name
ns.Annotations = info.Annotations ns.Annotations = info.Annotations
ns.Labels = portainerLabels
resourceQuota := &v1.ResourceQuota{ resourceQuota := &v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "portainer-rq-" + info.Name, Name: "portainer-rq-" + info.Name,
Namespace: info.Name, Namespace: info.Name,
Labels: portainerLabels,
}, },
Spec: v1.ResourceQuotaSpec{ Spec: v1.ResourceQuotaSpec{
Hard: v1.ResourceList{}, Hard: v1.ResourceList{},

View File

@ -17,7 +17,7 @@
class="searchInput" class="searchInput"
ng-model="$ctrl.state.textFilter" ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()" ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for a namespace..." placeholder="Search..."
auto-focus auto-focus
ng-model-options="{ debounce: 300 }" ng-model-options="{ debounce: 300 }"
data-cy="k8sNamespace-namespaceSearchInput" data-cy="k8sNamespace-namespaceSearchInput"

View File

@ -5,6 +5,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries'; import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
import { useCurrentUser } from '@/react/hooks/useUser';
import { Widget, WidgetBody } from '@@/Widget'; import { Widget, WidgetBody } from '@@/Widget';
@ -30,6 +31,7 @@ export function CreateNamespaceForm() {
const { data: registries } = useEnvironmentRegistries(environmentId, { const { data: registries } = useEnvironmentRegistries(environmentId, {
hideDefault: true, hideDefault: true,
}); });
const { user } = useCurrentUser();
// for namespace create, show ingress classes that are allowed in the current environment. // for namespace create, show ingress classes that are allowed in the current environment.
// the ingressClasses show the none option, so we don't need to add it here. // the ingressClasses show the none option, so we don't need to add it here.
const { data: ingressClasses } = useIngressControllerClassMapQuery({ const { data: ingressClasses } = useIngressControllerClassMapQuery({
@ -65,7 +67,7 @@ export function CreateNamespaceForm() {
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleSubmit} onSubmit={(values) => handleSubmit(values, user.Username)}
validateOnMount validateOnMount
validationSchema={getNamespaceValidationSchema( validationSchema={getNamespaceValidationSchema(
memoryLimit, memoryLimit,
@ -78,9 +80,9 @@ export function CreateNamespaceForm() {
</Widget> </Widget>
); );
function handleSubmit(values: CreateNamespaceFormValues) { function handleSubmit(values: CreateNamespaceFormValues, userName: string) {
const createNamespacePayload: CreateNamespacePayload = const createNamespacePayload: CreateNamespacePayload =
transformFormValuesToNamespacePayload(values); transformFormValuesToNamespacePayload(values, userName);
const updateRegistriesPayload: UpdateRegistryPayload[] = const updateRegistriesPayload: UpdateRegistryPayload[] =
values.registries.flatMap((registryFormValues) => { values.registries.flatMap((registryFormValues) => {
// find the matching registry from the cluster registries // find the matching registry from the cluster registries

View File

@ -15,6 +15,7 @@ export type CreateNamespaceFormValues = {
export type CreateNamespacePayload = { export type CreateNamespacePayload = {
Name: string; Name: string;
Owner: string;
ResourceQuota: ResourceQuotaPayload; ResourceQuota: ResourceQuotaPayload;
}; };

View File

@ -1,12 +1,14 @@
import { CreateNamespaceFormValues, CreateNamespacePayload } from './types'; import { CreateNamespaceFormValues, CreateNamespacePayload } from './types';
export function transformFormValuesToNamespacePayload( export function transformFormValuesToNamespacePayload(
createNamespaceFormValues: CreateNamespaceFormValues createNamespaceFormValues: CreateNamespaceFormValues,
owner: string
): CreateNamespacePayload { ): CreateNamespacePayload {
const memoryInBytes = const memoryInBytes =
Number(createNamespaceFormValues.resourceQuota.memory) * 10 ** 6; Number(createNamespaceFormValues.resourceQuota.memory) * 10 ** 6;
return { return {
Name: createNamespaceFormValues.name, Name: createNamespaceFormValues.name,
Owner: owner,
ResourceQuota: { ResourceQuota: {
enabled: createNamespaceFormValues.resourceQuota.enabled, enabled: createNamespaceFormValues.resourceQuota.enabled,
cpu: createNamespaceFormValues.resourceQuota.cpu, cpu: createNamespaceFormValues.resourceQuota.cpu,

View File

@ -97,9 +97,7 @@ export function NamespaceInnerForm({
} }
errors={errors.registries} errors={errors.registries}
/> />
{storageClasses.length > 0 && ( {storageClasses.length > 0 && <StorageQuotaFormSection />}
<StorageQuotaFormSection storageClasses={storageClasses} />
)}
<NamespaceSummary <NamespaceSummary
initialValues={initialValues} initialValues={initialValues}
values={values} values={values}

View File

@ -8,6 +8,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { InlineLoader } from '@@/InlineLoader'; import { InlineLoader } from '@@/InlineLoader';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection'; import { FormSection } from '@@/form-components/FormSection';
import { TextTip } from '@@/Tip/TextTip';
import { RegistriesSelector } from './RegistriesSelector'; import { RegistriesSelector } from './RegistriesSelector';
@ -24,10 +25,13 @@ export function RegistriesFormSection({ values, onChange, errors }: Props) {
}); });
return ( return (
<FormSection title="Registries"> <FormSection title="Registries">
<TextTip color="blue" className="mb-2">
Define which registries can be used by users who have access to this
namespace.
</TextTip>
<FormControl <FormControl
inputId="registries" inputId="registries"
label="Select registries" label="Select registries"
required
errors={errors} errors={errors}
> >
{registriesQuery.isLoading && ( {registriesQuery.isLoading && (

View File

@ -35,22 +35,12 @@ export function ResourceQuotaFormSection({
return ( return (
<FormSection title="Resource Quota"> <FormSection title="Resource Quota">
{values.enabled ? ( <TextTip color="blue">
<TextTip color="blue"> A resource quota sets boundaries on the compute resources a namespace
A namespace is a logical abstraction of a Kubernetes cluster, to can use. It&apos;s good practice to set a quota for a namespace to
provide for more flexible management of resources. Best practice is to manage resources effectively. Alternatively, you can disable assigning a
set a quota assignment as this ensures greatest security/stability; quota for unrestricted access (not recommended).
alternatively, you can disable assigning a quota for unrestricted </TextTip>
access (not recommended).
</TextTip>
) : (
<TextTip color="blue">
A namespace is a logical abstraction of a Kubernetes cluster, to
provide for more flexible management of resources. Resource
over-commit is disabled, please assign a capped limit of resources to
this namespace.
</TextTip>
)}
<SwitchField <SwitchField
data-cy="k8sNamespaceCreate-resourceAssignmentToggle" data-cy="k8sNamespaceCreate-resourceAssignmentToggle"

View File

@ -1,15 +1,9 @@
import { StorageClass } from '@/react/portainer/environments/types';
import { FormSection } from '@@/form-components/FormSection'; import { FormSection } from '@@/form-components/FormSection';
import { TextTip } from '@@/Tip/TextTip'; import { TextTip } from '@@/Tip/TextTip';
import { StorageQuotaItem } from './StorageQuotaItem'; import { StorageQuotaItem } from './StorageQuotaItem';
interface Props { export function StorageQuotaFormSection() {
storageClasses: StorageClass[];
}
export function StorageQuotaFormSection({ storageClasses }: Props) {
return ( return (
<FormSection title="Storage"> <FormSection title="Storage">
<TextTip color="blue"> <TextTip color="blue">
@ -19,9 +13,7 @@ export function StorageQuotaFormSection({ storageClasses }: Props) {
this namespace. this namespace.
</TextTip> </TextTip>
{storageClasses.map((storageClass) => ( <StorageQuotaItem />
<StorageQuotaItem key={storageClass.Name} storageClass={storageClass} />
))}
</FormSection> </FormSection>
); );
} }

View File

@ -1,23 +1,18 @@
import { Database } from 'lucide-react'; import { Database } from 'lucide-react';
import { StorageClass } from '@/react/portainer/environments/types';
import { FeatureId } from '@/react/portainer/feature-flags/enums'; import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { Icon } from '@@/Icon'; import { Icon } from '@@/Icon';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { SwitchField } from '@@/form-components/SwitchField'; import { SwitchField } from '@@/form-components/SwitchField';
type Props = { export function StorageQuotaItem() {
storageClass: StorageClass;
};
export function StorageQuotaItem({ storageClass }: Props) {
return ( return (
<div key={storageClass.Name}> <div>
<FormSectionTitle> <FormSectionTitle>
<div className="vertical-center text-muted inline-flex gap-1 align-top"> <div className="vertical-center text-muted inline-flex gap-1 align-top">
<Icon icon={Database} className="!mt-0.5 flex-none" /> <Icon icon={Database} className="!mt-0.5 flex-none" />
<span>{storageClass.Name}</span> <span>standard</span>
</div> </div>
</FormSectionTitle> </FormSectionTitle>
<hr className="mt-2 mb-0 w-full" /> <hr className="mt-2 mb-0 w-full" />