1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

refactor(settings): kube settings panel [EE-5504] (#9079)

This commit is contained in:
Chaim Lev-Ari 2023-06-20 11:02:39 +07:00 committed by GitHub
parent 806e1fdffa
commit 7dc6a1559f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 401 additions and 138 deletions

View file

@ -6,7 +6,8 @@ import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { SwitchField } from '@@/form-components/SwitchField';
import { useToggledValue } from './useToggledValue';
import { useToggledValue } from '../useToggledValue';
import { DemoAlert } from './DemoAlert';
export function LogoFieldset() {

View file

@ -7,7 +7,8 @@ import { FormControl } from '@@/form-components/FormControl';
import { TextArea } from '@@/form-components/Input/Textarea';
import { SwitchField } from '@@/form-components/SwitchField';
import { useToggledValue } from './useToggledValue';
import { useToggledValue } from '../useToggledValue';
import { DemoAlert } from './DemoAlert';
export function ScreenBannerFieldset() {

View file

@ -0,0 +1,85 @@
import { useFormikContext } from 'formik';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { FormSection } from '@@/form-components/FormSection';
import { SwitchField } from '@@/form-components/SwitchField';
import { KubeNoteMinimumCharacters } from './KubeNoteMinimumCharacters';
import { FormValues } from './types';
export function DeploymentOptionsSection() {
const {
values: { globalDeploymentOptions: values },
setFieldValue,
} = useFormikContext<FormValues>();
const limitedFeature = isLimitedToBE(FeatureId.ENFORCE_DEPLOYMENT_OPTIONS);
return (
<FormSection title="Deployment Options">
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Enforce code-based deployment"
checked={values.hideAddWithForm}
name="toggle_hideAddWithForm"
featureId={FeatureId.ENFORCE_DEPLOYMENT_OPTIONS}
onChange={(value) => handleToggleAddWithForm(value)}
labelClass="col-sm-3 col-lg-2"
tooltip="Hides the 'Add with form' buttons and prevents adding/editing of resources via forms"
/>
</div>
</div>
{values.hideAddWithForm && (
<div className="form-group flex flex-col gap-y-1">
<div className="col-sm-12">
<SwitchField
label="Allow web editor and custom template use"
checked={!values.hideWebEditor}
name="toggle_hideWebEditor"
onChange={(value) =>
setFieldValue('globalDeploymentOptions.hideWebEditor', !value)
}
labelClass="col-sm-2 !pl-4"
/>
</div>
<div className="col-sm-12">
<SwitchField
label="Allow specifying of a manifest via a URL"
checked={!values.hideFileUpload}
name="toggle_hideFileUpload"
onChange={(value) =>
setFieldValue('globalDeploymentOptions.hideFileUpload', !value)
}
labelClass="col-sm-2 !pl-4"
/>
</div>
</div>
)}
{!limitedFeature && (
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Allow per environment override"
checked={values.perEnvOverride}
onChange={(value) =>
setFieldValue('globalDeploymentOptions.perEnvOverride', value)
}
name="toggle_perEnvOverride"
labelClass="col-sm-3 col-lg-2"
tooltip="Allows overriding of deployment options in the Cluster setup screen of each environment"
/>
</div>
</div>
)}
<KubeNoteMinimumCharacters />
</FormSection>
);
async function handleToggleAddWithForm(checked: boolean) {
await setFieldValue('globalDeploymentOptions.hideWebEditor', checked);
await setFieldValue('globalDeploymentOptions.hideFileUpload', checked);
await setFieldValue('globalDeploymentOptions.hideAddWithForm', checked);
}
}

View file

@ -0,0 +1,37 @@
import { Field, useField } from 'formik';
import { TextTip } from '@@/Tip/TextTip';
import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection';
import { Input } from '@@/form-components/Input';
export function HelmSection() {
const [{ name }, { error }] = useField<string>('helmRepositoryUrl');
return (
<FormSection title="Helm Repository">
<div className="mb-2">
<TextTip color="blue">
You can specify the URL to your own helm repository here. See the{' '}
<a
href="https://helm.sh/docs/topics/chart_repository/"
target="_blank"
rel="noreferrer"
>
official documentation
</a>{' '}
for more details.
</TextTip>
</div>
<FormControl label="URL" errors={error} inputId="helm-repo-url">
<Field
as={Input}
id="helm-repo-url"
name={name}
placeholder="https://charts.bitnami.com/bitnami"
/>
</FormControl>
</FormSection>
);
}

View file

@ -0,0 +1,45 @@
import { useField } from 'formik';
import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection';
import { PortainerSelect } from '@@/form-components/PortainerSelect';
const options = [
{
label: '1 day',
value: '24h',
},
{
label: '7 days',
value: `${24 * 7}h`,
},
{
label: '30 days',
value: `${24 * 30}h`,
},
{
label: '1 year',
value: `${24 * 30 * 12}h`,
},
{
label: 'No expiry',
value: '0',
},
] as const;
export function KubeConfigSection() {
const [{ value }, { error }, { setValue }] =
useField<string>('kubeconfigExpiry');
return (
<FormSection title="Kubeconfig">
<FormControl label="Kubeconfig expiry" errors={error}>
<PortainerSelect
value={value}
options={options}
onChange={(value) => value && setValue(value)}
/>
</FormControl>
</FormSection>
);
}

View file

@ -0,0 +1,61 @@
import { useField } from 'formik';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { FormControl } from '@@/form-components/FormControl';
import { SwitchField } from '@@/form-components/SwitchField';
import { Input } from '@@/form-components/Input';
import { useToggledValue } from '../useToggledValue';
export function KubeNoteMinimumCharacters() {
const [{ value }, { error }, { setValue }] = useField<number>(
'globalDeploymentOptions.minApplicationNoteLength'
);
const [isEnabled, setIsEnabled] = useToggledValue(
'globalDeploymentOptions.minApplicationNoteLength',
'globalDeploymentOptions.requireNoteOnApplications'
);
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Require a note on applications"
checked={isEnabled}
name="toggle_requireNoteOnApplications"
onChange={(value) => setIsEnabled(value)}
featureId={FeatureId.K8S_REQUIRE_NOTE_ON_APPLICATIONS}
labelClass="col-sm-3 col-lg-2"
tooltip={`${
isBE ? '' : 'BE allows entry of notes in Add/Edit application. '
}Using this will enforce entry of a note in Add/Edit application (and prevent complete clearing of it in Application details).`}
/>
</div>
</div>
{isEnabled && (
<FormControl
label={
<span className="pl-4">
Minimum number of characters note must have
</span>
}
errors={error}
>
<Input
name="minNoteLength"
type="number"
placeholder="50"
min="1"
max="9999"
value={value}
onChange={(e) => setValue(e.target.valueAsNumber)}
className="w-1/4"
/>
</FormControl>
)}
</>
);
}

View file

@ -0,0 +1,109 @@
import { Form, Formik } from 'formik';
import { useQueryClient } from 'react-query';
import kubeIcon from '@/assets/ico/kube.svg?c';
import { notifySuccess } from '@/portainer/services/notifications';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { LoadingButton } from '@@/buttons';
import { Widget } from '@@/Widget';
import { useSettings, useUpdateSettingsMutation } from '../../queries';
import { HelmSection } from './HelmSection';
import { KubeConfigSection } from './KubeConfigSection';
import { FormValues } from './types';
import { DeploymentOptionsSection } from './DeploymentOptionsSection';
import { validation } from './validation';
export function KubeSettingsPanel() {
const settingsQuery = useSettings();
const queryClient = useQueryClient();
const environmentId = useEnvironmentId(false);
const mutation = useUpdateSettingsMutation();
if (!settingsQuery.data) {
return null;
}
const initialValues: FormValues = {
helmRepositoryUrl: settingsQuery.data.HelmRepositoryURL || '',
kubeconfigExpiry: settingsQuery.data.KubeconfigExpiry || '0',
globalDeploymentOptions: settingsQuery.data.GlobalDeploymentOptions || {
requireNoteOnApplications: false,
minApplicationNoteLength: 0,
hideAddWithForm: false,
hideFileUpload: false,
hideWebEditor: false,
perEnvOverride: false,
},
};
return (
<div className="row">
<div className="col-sm-12">
<Widget>
<Widget.Title icon={kubeIcon} title="Kubernetes settings" />
<Widget.Body>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
validateOnMount
>
{() => (
<Form className="form-horizontal">
<HelmSection />
<KubeConfigSection />
<DeploymentOptionsSection />
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
isLoading={mutation.isLoading}
loadingText="Saving"
className="!ml-0"
>
Save Kubernetes Settings
</LoadingButton>
</div>
</div>
</Form>
)}
</Formik>
</Widget.Body>
</Widget>
</div>
</div>
);
function handleSubmit(values: FormValues) {
mutation.mutate(
{
HelmRepositoryURL: values.helmRepositoryUrl,
KubeconfigExpiry: values.kubeconfigExpiry,
GlobalDeploymentOptions: {
...values.globalDeploymentOptions,
requireNoteOnApplications:
values.globalDeploymentOptions.requireNoteOnApplications,
minApplicationNoteLength: values.globalDeploymentOptions
.requireNoteOnApplications
? values.globalDeploymentOptions.minApplicationNoteLength
: 0,
},
},
{
async onSuccess() {
if (environmentId) {
await queryClient.invalidateQueries([
'environments',
environmentId,
'deploymentOptions',
]);
}
notifySuccess('Success', 'Kubernetes settings updated');
},
}
);
}
}

View file

@ -0,0 +1 @@
export { KubeSettingsPanel } from './KubeSettingsPanel';

View file

@ -0,0 +1,12 @@
export interface FormValues {
helmRepositoryUrl: string;
kubeconfigExpiry: string;
globalDeploymentOptions: {
hideAddWithForm: boolean;
perEnvOverride: boolean;
hideWebEditor: boolean;
hideFileUpload: boolean;
requireNoteOnApplications: boolean;
minApplicationNoteLength: number;
};
}

View file

@ -0,0 +1,32 @@
import { SchemaOf, object, string, boolean, number } from 'yup';
import { isValidUrl } from '@@/form-components/validate-url';
import { FormValues } from './types';
export function validation(): SchemaOf<FormValues> {
return object().shape({
helmRepositoryUrl: string()
.default('')
.test('valid-url', 'Invalid URL', (value) => !value || isValidUrl(value)),
kubeconfigExpiry: string().required(),
globalDeploymentOptions: object().shape({
hideAddWithForm: boolean().required(),
perEnvOverride: boolean().required(),
hideWebEditor: boolean().required(),
hideFileUpload: boolean().required(),
requireNoteOnApplications: boolean().required(),
minApplicationNoteLength: number()
.typeError('Must be a number')
.default(0)
.when('requireNoteOnApplications', {
is: true,
then: (schema) =>
schema
.required()
.min(1, 'Value should be between 1 to 9999')
.max(9999, 'Value should be between 1 to 9999'),
}),
}),
});
}

View file

@ -1,10 +1,13 @@
import { useField } from 'formik';
import { useState } from 'react';
export function useToggledValue(fieldName: string) {
export function useToggledValue(
fieldName: string,
toggleFieldName = `${fieldName}Enabled`
) {
const [, { value }, { setValue }] = useField<string>(fieldName);
const [, { value: isEnabled }, { setValue: setIsEnabled }] =
useField<boolean>(`${fieldName}Enabled`);
useField<boolean>(toggleFieldName);
const [oldValue, setOldValue] = useState(value);
async function handleIsEnabledChange(enabled: boolean) {

View file

@ -130,6 +130,7 @@ export interface Settings {
AllowStackManagementForRegularUsers: boolean;
AllowDeviceMappingForRegularUsers: boolean;
AllowContainerCapabilitiesForRegularUsers: boolean;
GlobalDeploymentOptions?: GlobalDeploymentOptions;
Edge: {
PingInterval: number;
SnapshotInterval: number;
@ -148,6 +149,9 @@ interface GlobalDeploymentOptions {
hideWebEditor: boolean;
/** Hide the file upload option in the remaining visible forms */
hideFileUpload: boolean;
/** Make note on application add/edit screen required */
requireNoteOnApplications: boolean;
minApplicationNoteLength: number;
}
export interface PublicSettingsResponse {