mirror of https://github.com/portainer/portainer
refactor(settings): migrate view to react [EE-5509] (#9179)
parent
b93624fa1f
commit
0e9902fee9
|
@ -364,8 +364,7 @@ angular
|
|||
url: '/settings',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/settings/settings.html',
|
||||
controller: 'SettingsController',
|
||||
component: 'settingsView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import { withI18nSuspense } from '@/react-tools/withI18nSuspense';
|
|||
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
|
||||
import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
|
||||
import { BackupSettingsPanel } from '@/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel';
|
||||
import { SettingsView } from '@/react/portainer/settings/SettingsView/SettingsView';
|
||||
|
||||
import { wizardModule } from './wizard';
|
||||
import { teamsModule } from './teams';
|
||||
|
@ -54,4 +55,8 @@ export const viewsModule = angular
|
|||
.component(
|
||||
'backupSettingsPanel',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(BackupSettingsPanel))), [])
|
||||
)
|
||||
.component(
|
||||
'settingsView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(SettingsView))), [])
|
||||
).name;
|
||||
|
|
|
@ -2,6 +2,9 @@ import { Environment } from '@/react/portainer/environments/types';
|
|||
|
||||
export interface StateManager {
|
||||
updateEndpointState(endpoint: Environment): Promise<void>;
|
||||
updateLogo(logo: string): void;
|
||||
updateSnapshotInterval(interval: string): void;
|
||||
updateEnableTelemetry(enable: boolean): void;
|
||||
}
|
||||
|
||||
export interface IAuthenticationService {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<page-header title="'Settings'" breadcrumbs="['Settings']"> </page-header>
|
||||
|
||||
<application-settings-panel on-success="(handleSuccess)"></application-settings-panel>
|
||||
|
||||
<kube-settings-panel></kube-settings-panel>
|
||||
|
||||
<helm-cert-panel></helm-cert-panel>
|
||||
|
||||
<ssl-settings-panel></ssl-settings-panel>
|
||||
|
||||
<hidden-containers-panel></hidden-containers-panel>
|
||||
|
||||
<!-- backup -->
|
||||
<backup-settings-panel></backup-settings-panel>
|
|
@ -1,20 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.app').controller('SettingsController', SettingsController);
|
||||
|
||||
/* @ngInject */
|
||||
function SettingsController($scope, StateManager) {
|
||||
$scope.handleSuccess = handleSuccess;
|
||||
|
||||
$scope.state = {
|
||||
showHTTPS: !window.ddExtension,
|
||||
};
|
||||
|
||||
function handleSuccess(settings) {
|
||||
if (settings) {
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,23 +47,19 @@ export function ApplicationSettingsPanel({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Title icon={SettingsIcon} title="Application settings" />
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm isLoading={mutation.isLoading} />
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
<Widget>
|
||||
<Widget.Title icon={SettingsIcon} title="Application settings" />
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm isLoading={mutation.isLoading} />
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
|
||||
function handleSubmit(values: Values) {
|
||||
|
|
|
@ -13,35 +13,31 @@ export function BackupSettingsPanel() {
|
|||
const [backupType, setBackupType] = useState(options[0].value);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<WidgetTitle icon={Download} title="Backup up Portainer" />
|
||||
<WidgetBody>
|
||||
<div className="form-horizontal">
|
||||
<FormSection title="Backup configuration">
|
||||
<div className="form-group col-sm-12 text-muted small">
|
||||
This will back up your Portainer server configuration and does
|
||||
not include containers.
|
||||
</div>
|
||||
<BoxSelector
|
||||
slim
|
||||
options={options}
|
||||
value={backupType}
|
||||
onChange={(v) => setBackupType(v)}
|
||||
radioName="backup-type"
|
||||
/>
|
||||
|
||||
{backupType === BackupFormType.S3 ? (
|
||||
<BackupS3Form />
|
||||
) : (
|
||||
<BackupFileForm />
|
||||
)}
|
||||
</FormSection>
|
||||
<Widget>
|
||||
<WidgetTitle icon={Download} title="Backup up Portainer" />
|
||||
<WidgetBody>
|
||||
<div className="form-horizontal">
|
||||
<FormSection title="Backup configuration">
|
||||
<div className="form-group col-sm-12 text-muted small">
|
||||
This will back up your Portainer server configuration and does not
|
||||
include containers.
|
||||
</div>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
<BoxSelector
|
||||
slim
|
||||
options={options}
|
||||
value={backupType}
|
||||
onChange={(v) => setBackupType(v)}
|
||||
radioName="backup-type"
|
||||
/>
|
||||
|
||||
{backupType === BackupFormType.S3 ? (
|
||||
<BackupS3Form />
|
||||
) : (
|
||||
<BackupFileForm />
|
||||
)}
|
||||
</FormSection>
|
||||
</div>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
|
||||
const fieldKey = 'OpenAIIntegration';
|
||||
|
||||
export function EnableOpenAIIntegrationSwitch() {
|
||||
const [inputProps, meta, helpers] = useField<boolean>(fieldKey);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
inputId="experimental_openAI"
|
||||
label="Enable OpenAI integration"
|
||||
size="medium"
|
||||
errors={meta.error}
|
||||
>
|
||||
<Switch
|
||||
id="experimental_openAI"
|
||||
name={fieldKey}
|
||||
className="space-right"
|
||||
checked={inputProps.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
function handleChange(enable: boolean) {
|
||||
helpers.setValue(enable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { FlaskConical } from 'lucide-react';
|
||||
|
||||
import { useExperimentalSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
|
||||
import { ExperimentalFeaturesSettingsForm } from './ExperimentalFeaturesForm';
|
||||
|
||||
export function ExperimentalFeatures() {
|
||||
const settingsQuery = useExperimentalSettings();
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = settingsQuery.data;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetTitle icon={FlaskConical} title="Experimental features" />
|
||||
<WidgetBody>
|
||||
<ExperimentalFeaturesSettingsForm
|
||||
settings={settings.experimentalFeatures}
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { Form, Formik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import { useCallback } from 'react';
|
||||
import { FlaskConical } from 'lucide-react';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { ExperimentalFeatures } from '@/react/portainer/settings/types';
|
||||
import { useUpdateExperimentalSettingsMutation } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { EnableOpenAIIntegrationSwitch } from './EnableOpenAIIntegrationSwitch';
|
||||
|
||||
interface FormValues {
|
||||
OpenAIIntegration: boolean;
|
||||
}
|
||||
const validation = yup.object({
|
||||
OpenAIIntegration: yup.boolean(),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
settings: ExperimentalFeatures;
|
||||
}
|
||||
|
||||
export function ExperimentalFeaturesSettingsForm({ settings }: Props) {
|
||||
const initialValues: FormValues = settings;
|
||||
|
||||
const mutation = useUpdateExperimentalSettingsMutation();
|
||||
|
||||
const { mutate: updateSettings } = mutation;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(variables: FormValues) => {
|
||||
updateSettings(
|
||||
{
|
||||
OpenAIIntegration: variables.OpenAIIntegration,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess(
|
||||
'Success',
|
||||
'Successfully updated experimental features settings'
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
enableReinitialize
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="form-horizontal">
|
||||
<TextTip color="blue" icon={FlaskConical}>
|
||||
Experimental features may be discontinued without notice.
|
||||
</TextTip>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="form-group col-sm-12 text-muted small">
|
||||
In Portainer releases, we may introduce features that we're
|
||||
experimenting with. These will be items in the early phases of
|
||||
development with limited testing.
|
||||
<br />
|
||||
Our goal is to gain early user feedback, so we can refine, enhance
|
||||
and ultimately make our features the best they can be. Disabling an
|
||||
experimental feature will prevent access to it.
|
||||
</div>
|
||||
|
||||
<EnableOpenAIIntegrationSwitch />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
loadingText="Saving settings..."
|
||||
isLoading={mutation.isLoading}
|
||||
disabled={!isValid || !dirty}
|
||||
className="!ml-0"
|
||||
data-cy="settings-experimentalButton"
|
||||
>
|
||||
Save experimental settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { ExperimentalFeatures } from './ExperimentalFeatures';
|
|
@ -30,28 +30,24 @@ export function HelmCertPanel() {
|
|||
};
|
||||
|
||||
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}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm isLoading={mutation.isLoading} />
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</BEOverlay>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
validateOnMount
|
||||
>
|
||||
<InnerForm isLoading={mutation.isLoading} />
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</BEOverlay>
|
||||
);
|
||||
|
||||
function handleSubmit({ clientCertFile }: FormValues) {
|
||||
|
|
|
@ -21,36 +21,30 @@ export function HiddenContainersPanel() {
|
|||
|
||||
const labels = settingsQuery.data;
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Title icon={Box} title="Hidden containers" />
|
||||
<Widget.Body>
|
||||
<div className="mb-3">
|
||||
<TextTip color="blue">
|
||||
You can hide containers with specific labels from Portainer UI.
|
||||
You need to specify the label name and value.
|
||||
</TextTip>
|
||||
</div>
|
||||
<Widget>
|
||||
<Widget.Title icon={Box} title="Hidden containers" />
|
||||
<Widget.Body>
|
||||
<div className="mb-3">
|
||||
<TextTip color="blue">
|
||||
You can hide containers with specific labels from Portainer UI. You
|
||||
need to specify the label name and value.
|
||||
</TextTip>
|
||||
</div>
|
||||
|
||||
<AddLabelForm
|
||||
isLoading={mutation.isLoading}
|
||||
onSubmit={(name, value) =>
|
||||
handleSubmit([...labels, { name, value }])
|
||||
}
|
||||
/>
|
||||
<AddLabelForm
|
||||
isLoading={mutation.isLoading}
|
||||
onSubmit={(name, value) => handleSubmit([...labels, { name, value }])}
|
||||
/>
|
||||
|
||||
<HiddenContainersTable
|
||||
labels={labels}
|
||||
isLoading={mutation.isLoading}
|
||||
onDelete={(name) =>
|
||||
handleSubmit(labels.filter((label) => label.name !== name))
|
||||
}
|
||||
/>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
<HiddenContainersTable
|
||||
labels={labels}
|
||||
isLoading={mutation.isLoading}
|
||||
onDelete={(name) =>
|
||||
handleSubmit(labels.filter((label) => label.name !== name))
|
||||
}
|
||||
/>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
|
||||
function handleSubmit(labels: Pair[]) {
|
||||
|
|
|
@ -40,41 +40,37 @@ export function KubeSettingsPanel() {
|
|||
};
|
||||
|
||||
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 />
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
|
|
|
@ -43,99 +43,95 @@ function SSLSettingsPanel() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Title icon={Key} title="SSL certificate" />
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
{({ values, setFieldValue, isValid, errors }) => (
|
||||
<Form className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="orange">
|
||||
Forcing HTTPs only will cause Portainer to stop
|
||||
listening on the HTTP port. Any edge agent environment
|
||||
that is using HTTP will no longer be available.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
<Widget>
|
||||
<Widget.Title icon={Key} title="SSL certificate" />
|
||||
<Widget.Body>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
>
|
||||
{({ values, setFieldValue, isValid, errors }) => (
|
||||
<Form className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="orange">
|
||||
Forcing HTTPs only will cause Portainer to stop listening on
|
||||
the HTTP port. Any edge agent environment that is using HTTP
|
||||
will no longer be available.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
checked={values.forceHTTPS}
|
||||
label="Force HTTPS only"
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
name="forceHTTPS"
|
||||
onChange={(value) => setFieldValue('forceHTTPS', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
checked={values.forceHTTPS}
|
||||
label="Force HTTPS only"
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
name="forceHTTPS"
|
||||
onChange={(value) => setFieldValue('forceHTTPS', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="blue">
|
||||
Provide a new SSL Certificate to replace the existing
|
||||
one that is used for HTTPS connections.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="blue">
|
||||
Provide a new SSL Certificate to replace the existing one
|
||||
that is used for HTTPS connections.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="SSL/TLS certificate"
|
||||
tooltip="Select an X.509 certificate file, commonly a crt, cer or pem file."
|
||||
inputId="ca-cert-field"
|
||||
errors={errors.certFile}
|
||||
<FormControl
|
||||
label="SSL/TLS certificate"
|
||||
tooltip="Select an X.509 certificate file, commonly a crt, cer or pem file."
|
||||
inputId="ca-cert-field"
|
||||
errors={errors.certFile}
|
||||
>
|
||||
<FileUploadField
|
||||
required={typeof errors.certFile !== 'undefined'}
|
||||
inputId="ca-cert-field"
|
||||
name="certFile"
|
||||
onChange={(file) => setFieldValue('certFile', file)}
|
||||
value={values.certFile}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="SSL/TLS private key"
|
||||
tooltip="Select a private key file, commonly a key, or pem file."
|
||||
inputId="ca-cert-field"
|
||||
errors={errors.keyFile}
|
||||
>
|
||||
<FileUploadField
|
||||
required={typeof errors.keyFile !== 'undefined'}
|
||||
inputId="ca-cert-field"
|
||||
name="keyFile"
|
||||
onChange={(file) => setFieldValue('keyFile', file)}
|
||||
value={values.keyFile}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
isLoading={mutation.isLoading || reloadingPage}
|
||||
disabled={!isValid}
|
||||
loadingText={reloadingPage ? 'Reloading' : 'Saving'}
|
||||
className="!ml-0"
|
||||
>
|
||||
<FileUploadField
|
||||
required={typeof errors.certFile !== 'undefined'}
|
||||
inputId="ca-cert-field"
|
||||
name="certFile"
|
||||
onChange={(file) => setFieldValue('certFile', file)}
|
||||
value={values.certFile}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="SSL/TLS private key"
|
||||
tooltip="Select a private key file, commonly a key, or pem file."
|
||||
inputId="ca-cert-field"
|
||||
errors={errors.keyFile}
|
||||
>
|
||||
<FileUploadField
|
||||
required={typeof errors.keyFile !== 'undefined'}
|
||||
inputId="ca-cert-field"
|
||||
name="keyFile"
|
||||
onChange={(file) => setFieldValue('keyFile', file)}
|
||||
value={values.keyFile}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
isLoading={mutation.isLoading || reloadingPage}
|
||||
disabled={!isValid}
|
||||
loadingText={reloadingPage ? 'Reloading' : 'Saving'}
|
||||
className="!ml-0"
|
||||
>
|
||||
Save SSL Settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
Save SSL Settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
|
||||
function handleSubmit({ certFile, forceHTTPS, keyFile }: FormValues) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { StateManager } from '@/portainer/services/types';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { Settings } from '../types';
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
|
||||
import { ApplicationSettingsPanel } from './ApplicationSettingsPanel';
|
||||
import { BackupSettingsPanel } from './BackupSettingsView';
|
||||
import { HelmCertPanel } from './HelmCertPanel';
|
||||
import { HiddenContainersPanel } from './HiddenContainersPanel/HiddenContainersPanel';
|
||||
import { KubeSettingsPanel } from './KubeSettingsPanel';
|
||||
import { SSLSettingsPanelWrapper } from './SSLSettingsPanel/SSLSettingsPanel';
|
||||
import { ExperimentalFeatures } from './ExperimentalFeatures';
|
||||
|
||||
export function SettingsView() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Settings" breadcrumbs="Settings" />
|
||||
|
||||
<div className="mx-4 space-y-4">
|
||||
<ApplicationSettingsPanel onSuccess={handleSuccess} />
|
||||
|
||||
<KubeSettingsPanel />
|
||||
|
||||
<HelmCertPanel />
|
||||
|
||||
<SSLSettingsPanelWrapper />
|
||||
|
||||
{isBE && <ExperimentalFeatures />}
|
||||
|
||||
<HiddenContainersPanel />
|
||||
|
||||
<BackupSettingsPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function handleSuccess(settings: Settings) {
|
||||
// to sync "outside state" - for angularjs
|
||||
// this is a hack, but it works
|
||||
// state manager should be replaced with a non angular solution, maybe using zustand
|
||||
const $injector = angular.element(document).injector();
|
||||
$injector.invoke(
|
||||
/* @ngInject */ (StateManager: StateManager) => {
|
||||
StateManager?.updateLogo(settings.LogoURL);
|
||||
StateManager?.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
StateManager?.updateEnableTelemetry(settings.EnableTelemetry);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export { queryKeys } from './queryKeys';
|
||||
export {
|
||||
useSettings,
|
||||
useUpdateDefaultRegistrySettingsMutation,
|
||||
useUpdateSettingsMutation,
|
||||
} from './useSettings';
|
||||
export { usePublicSettings } from './usePublicSettings';
|
||||
export { useExperimentalSettings } from './useExperimentalSettings';
|
||||
export { useUpdateExperimentalSettingsMutation } from './useExperimentalSettingsMutation';
|
|
@ -0,0 +1,5 @@
|
|||
export const queryKeys = {
|
||||
base: () => ['settings'] as const,
|
||||
public: () => [...queryKeys.base(), 'public'] as const,
|
||||
experimental: () => [...queryKeys.base(), 'experimental'] as const,
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { ExperimentalFeatures } from '../types';
|
||||
import { buildUrl } from '../settings.service';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
type ExperimentalFeaturesSettings = {
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
};
|
||||
|
||||
export function useExperimentalSettings<T = ExperimentalFeaturesSettings>(
|
||||
select?: (settings: ExperimentalFeaturesSettings) => T,
|
||||
enabled = true
|
||||
) {
|
||||
return useQuery(queryKeys.experimental(), getExperimentalSettings, {
|
||||
select,
|
||||
enabled,
|
||||
staleTime: 50,
|
||||
...withError('Unable to retrieve experimental settings'),
|
||||
});
|
||||
}
|
||||
|
||||
async function getExperimentalSettings() {
|
||||
try {
|
||||
const { data } = await axios.get<ExperimentalFeaturesSettings>(
|
||||
buildUrl('experimental')
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to retrieve experimental settings'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import {
|
||||
mutationOptions,
|
||||
withError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
|
||||
import { ExperimentalFeatures } from '../types';
|
||||
import { buildUrl } from '../settings.service';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export function useUpdateExperimentalSettingsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
updateExperimentalSettings,
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [queryKeys.base()]),
|
||||
withError('Unable to update experimental settings')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function updateExperimentalSettings(
|
||||
settings: Partial<ExperimentalFeatures>
|
||||
) {
|
||||
try {
|
||||
await axios.put(buildUrl('experimental'), settings);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update experimental settings');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { getPublicSettings } from '../settings.service';
|
||||
import { PublicSettingsResponse } from '../types';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export function usePublicSettings<T = PublicSettingsResponse>({
|
||||
enabled,
|
||||
select,
|
||||
onSuccess,
|
||||
}: {
|
||||
select?: (settings: PublicSettingsResponse) => T;
|
||||
enabled?: boolean;
|
||||
onSuccess?: (data: T) => void;
|
||||
} = {}) {
|
||||
return useQuery(queryKeys.public(), getPublicSettings, {
|
||||
select,
|
||||
...withError('Unable to retrieve public settings'),
|
||||
enabled,
|
||||
onSuccess,
|
||||
});
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import {
|
||||
mutationOptions,
|
||||
|
@ -7,37 +7,22 @@ import {
|
|||
} from '@/react-tools/react-query';
|
||||
|
||||
import {
|
||||
getSettings,
|
||||
updateSettings,
|
||||
getPublicSettings,
|
||||
getSettings,
|
||||
updateDefaultRegistry,
|
||||
} from './settings.service';
|
||||
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
|
||||
} from '../settings.service';
|
||||
import { DefaultRegistry, Settings } from '../types';
|
||||
|
||||
export function usePublicSettings<T = PublicSettingsResponse>({
|
||||
enabled,
|
||||
select,
|
||||
onSuccess,
|
||||
}: {
|
||||
select?: (settings: PublicSettingsResponse) => T;
|
||||
enabled?: boolean;
|
||||
onSuccess?: (data: T) => void;
|
||||
} = {}) {
|
||||
return useQuery(['settings', 'public'], getPublicSettings, {
|
||||
select,
|
||||
...withError('Unable to retrieve public settings'),
|
||||
enabled,
|
||||
onSuccess,
|
||||
});
|
||||
}
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
export function useSettings<T = Settings>(
|
||||
select?: (settings: Settings) => T,
|
||||
enabled?: boolean
|
||||
enabled = true
|
||||
) {
|
||||
return useQuery(['settings'], getSettings, {
|
||||
return useQuery(queryKeys.base(), getSettings, {
|
||||
select,
|
||||
enabled,
|
||||
staleTime: 50,
|
||||
...withError('Unable to retrieve settings'),
|
||||
});
|
||||
}
|
||||
|
@ -48,7 +33,7 @@ export function useUpdateSettingsMutation() {
|
|||
return useMutation(
|
||||
updateSettings,
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [['settings'], ['cloud']]),
|
||||
withInvalidate(queryClient, [queryKeys.base(), ['cloud']]),
|
||||
withError('Unable to update settings')
|
||||
)
|
||||
);
|
||||
|
@ -60,7 +45,7 @@ export function useUpdateDefaultRegistrySettingsMutation() {
|
|||
return useMutation(
|
||||
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [['settings']]),
|
||||
withInvalidate(queryClient, [queryKeys.base()]),
|
||||
withError('Unable to update default registry settings')
|
||||
)
|
||||
);
|
|
@ -16,6 +16,11 @@ export async function getPublicSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getGlobalDeploymentOptions() {
|
||||
const publicSettings = await getPublicSettings();
|
||||
return publicSettings.GlobalDeploymentOptions;
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
try {
|
||||
const { data } = await axios.get<Settings>(buildUrl());
|
||||
|
@ -54,7 +59,7 @@ export async function updateDefaultRegistry(
|
|||
}
|
||||
}
|
||||
|
||||
function buildUrl(subResource?: string, action?: string) {
|
||||
export function buildUrl(subResource?: string, action?: string) {
|
||||
let url = 'settings';
|
||||
if (subResource) {
|
||||
url += `/${subResource}`;
|
||||
|
|
|
@ -93,6 +93,10 @@ export interface DefaultRegistry {
|
|||
Hide: boolean;
|
||||
}
|
||||
|
||||
export interface ExperimentalFeatures {
|
||||
OpenAIIntegration: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
LogoURL: string;
|
||||
CustomLoginBanner: string;
|
||||
|
@ -123,6 +127,7 @@ export interface Settings {
|
|||
DisplayDonationHeader: boolean;
|
||||
DisplayExternalContributors: boolean;
|
||||
EnableHostManagementFeatures: boolean;
|
||||
ExperimentalFeatures?: ExperimentalFeatures;
|
||||
AllowVolumeBrowserForRegularUsers: boolean;
|
||||
AllowBindMountsForRegularUsers: boolean;
|
||||
AllowPrivilegedModeForRegularUsers: boolean;
|
||||
|
|
Loading…
Reference in New Issue