mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
refactor(settings): migrate helm cert panel to react [EE-5505] (#9132)
This commit is contained in:
parent
c452de82b7
commit
f293ea41d3
19 changed files with 268 additions and 70 deletions
|
@ -22,5 +22,4 @@
|
|||
|
||||
.be-indicator-container {
|
||||
border: solid 1px var(--BE-only);
|
||||
margin: 15px;
|
||||
}
|
||||
|
|
31
app/react/components/BEFeatureIndicator/BEOverlay.tsx
Normal file
31
app/react/components/BEFeatureIndicator/BEOverlay.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
import { BEFeatureIndicator } from '.';
|
||||
|
||||
export function BEOverlay({
|
||||
featureId,
|
||||
children,
|
||||
}: {
|
||||
featureId: FeatureId;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const isLimited = isLimitedToBE(featureId);
|
||||
if (!isLimited) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="be-indicator-container limited-be">
|
||||
<div className="overlay">
|
||||
<div className="limited-be-link vertical-center">
|
||||
<BEFeatureIndicator featureId={FeatureId.CA_FILE} />
|
||||
<Tooltip message="This feature is currently limited to Business Edition users only. " />
|
||||
</div>
|
||||
<div className="limited-be-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,7 @@ export interface Props {
|
|||
required?: boolean;
|
||||
inputId: string;
|
||||
color?: ComponentProps<typeof Button>['color'];
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function FileUploadField({
|
||||
|
@ -24,6 +25,7 @@ export function FileUploadField({
|
|||
required = false,
|
||||
inputId,
|
||||
color = 'primary',
|
||||
name,
|
||||
}: Props) {
|
||||
const fileRef = createRef<HTMLInputElement>();
|
||||
|
||||
|
@ -38,6 +40,7 @@ export function FileUploadField({
|
|||
className={styles.fileInput}
|
||||
onChange={changeHandler}
|
||||
aria-label="file-input"
|
||||
name={name}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
|
|
34
app/react/components/form-components/FormActions.tsx
Normal file
34
app/react/components/form-components/FormActions.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { LoadingButton } from '@@/buttons';
|
||||
|
||||
interface Props {
|
||||
submitLabel: string;
|
||||
loadingText: string;
|
||||
isLoading: boolean;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export function FormActions({
|
||||
submitLabel = 'Save',
|
||||
loadingText = 'Saving',
|
||||
isLoading,
|
||||
children,
|
||||
isValid,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
loadingText={loadingText}
|
||||
isLoading={isLoading}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{submitLabel}
|
||||
</LoadingButton>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -22,3 +22,24 @@ export function withFileSize(fileValidation: FileSchema, maxSize: number) {
|
|||
return file.size <= maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
export function withFileExtension(
|
||||
fileValidation: FileSchema,
|
||||
allowedExtensions: string[]
|
||||
) {
|
||||
return fileValidation.test(
|
||||
'fileExtension',
|
||||
'Selected file has invalid extension.',
|
||||
validateFileExtension
|
||||
);
|
||||
|
||||
function validateFileExtension(file?: File) {
|
||||
if (!file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
|
||||
return allowedExtensions.includes(fileExtension || '');
|
||||
}
|
||||
}
|
||||
|
|
121
app/react/portainer/settings/SettingsView/HelmCertPanel.tsx
Normal file
121
app/react/portainer/settings/SettingsView/HelmCertPanel.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { Form, Formik, useFormikContext } from 'formik';
|
||||
import { Key } from 'lucide-react';
|
||||
import { SchemaOf, object } from 'yup';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { FileUploadField } from '@@/form-components/FileUpload';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import {
|
||||
file,
|
||||
withFileExtension,
|
||||
} from '@@/form-components/yup-file-validation';
|
||||
import { FormActions } from '@@/form-components/FormActions';
|
||||
import { BEOverlay } from '@@/BEFeatureIndicator/BEOverlay';
|
||||
|
||||
import { FeatureId } from '../../feature-flags/enums';
|
||||
|
||||
import { useUpdateSSLConfigMutation } from './useUpdateSSLConfigMutation';
|
||||
|
||||
interface FormValues {
|
||||
clientCertFile: File | null;
|
||||
}
|
||||
|
||||
export function HelmCertPanel() {
|
||||
const mutation = useUpdateSSLConfigMutation();
|
||||
const initialValues = {
|
||||
clientCertFile: null,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<BEOverlay featureId={FeatureId.CA_FILE}>
|
||||
<Widget>
|
||||
<Widget.Title
|
||||
icon={Key}
|
||||
title="Certificate Authority file for Kubernetes Helm repositories"
|
||||
/>
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validation}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<InnerForm isLoading={mutation.isLoading} />
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</BEOverlay>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleSubmit({ clientCertFile }: FormValues) {
|
||||
if (!clientCertFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate(
|
||||
{ clientCertFile },
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess('Success', 'Helm certificate updated');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function InnerForm({ isLoading }: { isLoading: boolean }) {
|
||||
const { values, setFieldValue, errors, isValid } =
|
||||
useFormikContext<FormValues>();
|
||||
|
||||
return (
|
||||
<Form className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="blue">
|
||||
Provide an additional CA file containing certificate(s) for HTTPS
|
||||
connections to Helm repositories.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="CA file"
|
||||
tooltip="Select a CA file containing your X.509 certificate(s), commonly a crt, cer or pem file."
|
||||
inputId="ca-cert-field"
|
||||
errors={errors?.clientCertFile}
|
||||
>
|
||||
<FileUploadField
|
||||
required
|
||||
inputId="ca-cert-field"
|
||||
name="clientCertFile"
|
||||
onChange={(file) => setFieldValue('clientCertFile', file)}
|
||||
value={values.clientCertFile}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormActions
|
||||
isValid={isValid}
|
||||
isLoading={isLoading}
|
||||
submitLabel="Apply changes"
|
||||
loadingText="Saving in progress..."
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function validation(): SchemaOf<FormValues> {
|
||||
return object({
|
||||
clientCertFile: withFileExtension(file(), [
|
||||
'pem',
|
||||
'crt',
|
||||
'cer',
|
||||
'cert',
|
||||
]).required(),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { useMutation } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { mutationOptions, withError } from '@/react-tools/react-query';
|
||||
|
||||
export function useUpdateSSLConfigMutation() {
|
||||
return useMutation(
|
||||
updateSSLConfig,
|
||||
mutationOptions(withError('Unable to update SSL configuration'))
|
||||
);
|
||||
}
|
||||
|
||||
interface SSLConfig {
|
||||
// SSL Certificates
|
||||
certFile?: File;
|
||||
keyFile?: File;
|
||||
httpEnabled?: boolean;
|
||||
|
||||
// SSL Client Certificates
|
||||
clientCertFile?: File;
|
||||
}
|
||||
|
||||
async function updateSSLConfig({
|
||||
certFile,
|
||||
keyFile,
|
||||
clientCertFile,
|
||||
...payload
|
||||
}: SSLConfig) {
|
||||
try {
|
||||
const cert = certFile ? await certFile.text() : undefined;
|
||||
const key = keyFile ? await keyFile.text() : undefined;
|
||||
const clientCert = clientCertFile ? await clientCertFile.text() : undefined;
|
||||
|
||||
await axios.put('/ssl', {
|
||||
...payload,
|
||||
cert,
|
||||
key,
|
||||
clientCert,
|
||||
});
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error, 'Unable to update SSL configuration');
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue