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',
|
url: '/settings',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: './views/settings/settings.html',
|
component: 'settingsView',
|
||||||
controller: 'SettingsController',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { withI18nSuspense } from '@/react-tools/withI18nSuspense';
|
||||||
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
|
import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView';
|
||||||
import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
|
import { ListView as EnvironmentsListView } from '@/react/portainer/environments/ListView';
|
||||||
import { BackupSettingsPanel } from '@/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel';
|
import { BackupSettingsPanel } from '@/react/portainer/settings/SettingsView/BackupSettingsView/BackupSettingsPanel';
|
||||||
|
import { SettingsView } from '@/react/portainer/settings/SettingsView/SettingsView';
|
||||||
|
|
||||||
import { wizardModule } from './wizard';
|
import { wizardModule } from './wizard';
|
||||||
import { teamsModule } from './teams';
|
import { teamsModule } from './teams';
|
||||||
|
@ -54,4 +55,8 @@ export const viewsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'backupSettingsPanel',
|
'backupSettingsPanel',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(BackupSettingsPanel))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(BackupSettingsPanel))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'settingsView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(SettingsView))), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
export interface StateManager {
|
export interface StateManager {
|
||||||
updateEndpointState(endpoint: Environment): Promise<void>;
|
updateEndpointState(endpoint: Environment): Promise<void>;
|
||||||
|
updateLogo(logo: string): void;
|
||||||
|
updateSnapshotInterval(interval: string): void;
|
||||||
|
updateEnableTelemetry(enable: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthenticationService {
|
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,8 +47,6 @@ export function ApplicationSettingsPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Title icon={SettingsIcon} title="Application settings" />
|
<Widget.Title icon={SettingsIcon} title="Application settings" />
|
||||||
<Widget.Body>
|
<Widget.Body>
|
||||||
|
@ -62,8 +60,6 @@ export function ApplicationSettingsPanel({
|
||||||
</Formik>
|
</Formik>
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit(values: Values) {
|
function handleSubmit(values: Values) {
|
||||||
|
|
|
@ -13,16 +13,14 @@ export function BackupSettingsPanel() {
|
||||||
const [backupType, setBackupType] = useState(options[0].value);
|
const [backupType, setBackupType] = useState(options[0].value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetTitle icon={Download} title="Backup up Portainer" />
|
<WidgetTitle icon={Download} title="Backup up Portainer" />
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<div className="form-horizontal">
|
<div className="form-horizontal">
|
||||||
<FormSection title="Backup configuration">
|
<FormSection title="Backup configuration">
|
||||||
<div className="form-group col-sm-12 text-muted small">
|
<div className="form-group col-sm-12 text-muted small">
|
||||||
This will back up your Portainer server configuration and does
|
This will back up your Portainer server configuration and does not
|
||||||
not include containers.
|
include containers.
|
||||||
</div>
|
</div>
|
||||||
<BoxSelector
|
<BoxSelector
|
||||||
slim
|
slim
|
||||||
|
@ -41,7 +39,5 @@ export function BackupSettingsPanel() {
|
||||||
</div>
|
</div>
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,8 +30,6 @@ export function HelmCertPanel() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<BEOverlay featureId={FeatureId.CA_FILE}>
|
<BEOverlay featureId={FeatureId.CA_FILE}>
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Title
|
<Widget.Title
|
||||||
|
@ -50,8 +48,6 @@ export function HelmCertPanel() {
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</BEOverlay>
|
</BEOverlay>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit({ clientCertFile }: FormValues) {
|
function handleSubmit({ clientCertFile }: FormValues) {
|
||||||
|
|
|
@ -21,23 +21,19 @@ export function HiddenContainersPanel() {
|
||||||
|
|
||||||
const labels = settingsQuery.data;
|
const labels = settingsQuery.data;
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Title icon={Box} title="Hidden containers" />
|
<Widget.Title icon={Box} title="Hidden containers" />
|
||||||
<Widget.Body>
|
<Widget.Body>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
You can hide containers with specific labels from Portainer UI.
|
You can hide containers with specific labels from Portainer UI. You
|
||||||
You need to specify the label name and value.
|
need to specify the label name and value.
|
||||||
</TextTip>
|
</TextTip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddLabelForm
|
<AddLabelForm
|
||||||
isLoading={mutation.isLoading}
|
isLoading={mutation.isLoading}
|
||||||
onSubmit={(name, value) =>
|
onSubmit={(name, value) => handleSubmit([...labels, { name, value }])}
|
||||||
handleSubmit([...labels, { name, value }])
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HiddenContainersTable
|
<HiddenContainersTable
|
||||||
|
@ -49,8 +45,6 @@ export function HiddenContainersPanel() {
|
||||||
/>
|
/>
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit(labels: Pair[]) {
|
function handleSubmit(labels: Pair[]) {
|
||||||
|
|
|
@ -40,8 +40,6 @@ export function KubeSettingsPanel() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Title icon={kubeIcon} title="Kubernetes settings" />
|
<Widget.Title icon={kubeIcon} title="Kubernetes settings" />
|
||||||
<Widget.Body>
|
<Widget.Body>
|
||||||
|
@ -73,8 +71,6 @@ export function KubeSettingsPanel() {
|
||||||
</Formik>
|
</Formik>
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit(values: FormValues) {
|
function handleSubmit(values: FormValues) {
|
||||||
|
|
|
@ -43,8 +43,6 @@ function SSLSettingsPanel() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<Widget>
|
<Widget>
|
||||||
<Widget.Title icon={Key} title="SSL certificate" />
|
<Widget.Title icon={Key} title="SSL certificate" />
|
||||||
<Widget.Body>
|
<Widget.Body>
|
||||||
|
@ -59,9 +57,9 @@ function SSLSettingsPanel() {
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<TextTip color="orange">
|
<TextTip color="orange">
|
||||||
Forcing HTTPs only will cause Portainer to stop
|
Forcing HTTPs only will cause Portainer to stop listening on
|
||||||
listening on the HTTP port. Any edge agent environment
|
the HTTP port. Any edge agent environment that is using HTTP
|
||||||
that is using HTTP will no longer be available.
|
will no longer be available.
|
||||||
</TextTip>
|
</TextTip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,8 +79,8 @@ function SSLSettingsPanel() {
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
Provide a new SSL Certificate to replace the existing
|
Provide a new SSL Certificate to replace the existing one
|
||||||
one that is used for HTTPS connections.
|
that is used for HTTPS connections.
|
||||||
</TextTip>
|
</TextTip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,8 +132,6 @@ function SSLSettingsPanel() {
|
||||||
</Formik>
|
</Formik>
|
||||||
</Widget.Body>
|
</Widget.Body>
|
||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit({ certFile, forceHTTPS, keyFile }: FormValues) {
|
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 {
|
import {
|
||||||
mutationOptions,
|
mutationOptions,
|
||||||
|
@ -7,37 +7,22 @@ import {
|
||||||
} from '@/react-tools/react-query';
|
} from '@/react-tools/react-query';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSettings,
|
|
||||||
updateSettings,
|
updateSettings,
|
||||||
getPublicSettings,
|
getSettings,
|
||||||
updateDefaultRegistry,
|
updateDefaultRegistry,
|
||||||
} from './settings.service';
|
} from '../settings.service';
|
||||||
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
|
import { DefaultRegistry, Settings } from '../types';
|
||||||
|
|
||||||
export function usePublicSettings<T = PublicSettingsResponse>({
|
import { queryKeys } from './queryKeys';
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSettings<T = Settings>(
|
export function useSettings<T = Settings>(
|
||||||
select?: (settings: Settings) => T,
|
select?: (settings: Settings) => T,
|
||||||
enabled?: boolean
|
enabled = true
|
||||||
) {
|
) {
|
||||||
return useQuery(['settings'], getSettings, {
|
return useQuery(queryKeys.base(), getSettings, {
|
||||||
select,
|
select,
|
||||||
enabled,
|
enabled,
|
||||||
|
staleTime: 50,
|
||||||
...withError('Unable to retrieve settings'),
|
...withError('Unable to retrieve settings'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,7 +33,7 @@ export function useUpdateSettingsMutation() {
|
||||||
return useMutation(
|
return useMutation(
|
||||||
updateSettings,
|
updateSettings,
|
||||||
mutationOptions(
|
mutationOptions(
|
||||||
withInvalidate(queryClient, [['settings'], ['cloud']]),
|
withInvalidate(queryClient, [queryKeys.base(), ['cloud']]),
|
||||||
withError('Unable to update settings')
|
withError('Unable to update settings')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -60,7 +45,7 @@ export function useUpdateDefaultRegistrySettingsMutation() {
|
||||||
return useMutation(
|
return useMutation(
|
||||||
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
|
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
|
||||||
mutationOptions(
|
mutationOptions(
|
||||||
withInvalidate(queryClient, [['settings']]),
|
withInvalidate(queryClient, [queryKeys.base()]),
|
||||||
withError('Unable to update default registry settings')
|
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() {
|
export async function getSettings() {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<Settings>(buildUrl());
|
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';
|
let url = 'settings';
|
||||||
if (subResource) {
|
if (subResource) {
|
||||||
url += `/${subResource}`;
|
url += `/${subResource}`;
|
||||||
|
|
|
@ -93,6 +93,10 @@ export interface DefaultRegistry {
|
||||||
Hide: boolean;
|
Hide: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExperimentalFeatures {
|
||||||
|
OpenAIIntegration: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
LogoURL: string;
|
LogoURL: string;
|
||||||
CustomLoginBanner: string;
|
CustomLoginBanner: string;
|
||||||
|
@ -123,6 +127,7 @@ export interface Settings {
|
||||||
DisplayDonationHeader: boolean;
|
DisplayDonationHeader: boolean;
|
||||||
DisplayExternalContributors: boolean;
|
DisplayExternalContributors: boolean;
|
||||||
EnableHostManagementFeatures: boolean;
|
EnableHostManagementFeatures: boolean;
|
||||||
|
ExperimentalFeatures?: ExperimentalFeatures;
|
||||||
AllowVolumeBrowserForRegularUsers: boolean;
|
AllowVolumeBrowserForRegularUsers: boolean;
|
||||||
AllowBindMountsForRegularUsers: boolean;
|
AllowBindMountsForRegularUsers: boolean;
|
||||||
AllowPrivilegedModeForRegularUsers: boolean;
|
AllowPrivilegedModeForRegularUsers: boolean;
|
||||||
|
|
Loading…
Reference in New Issue