refactor(settings): migrate view to react [EE-5509] (#9179)

pull/8937/head
Chaim Lev-Ari 2023-07-13 10:46:12 +03:00 committed by GitHub
parent b93624fa1f
commit 0e9902fee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 548 additions and 281 deletions

View File

@ -364,8 +364,7 @@ angular
url: '/settings',
views: {
'content@': {
templateUrl: './views/settings/settings.html',
controller: 'SettingsController',
component: 'settingsView',
},
},
};

View File

@ -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;

View File

@ -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 {

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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>
);
}

View File

@ -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);
}
}

View File

@ -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>
);
}

View File

@ -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&apos;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>
);
}

View File

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

View File

@ -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) {

View File

@ -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[]) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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);
}
);
}

View File

@ -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';

View File

@ -0,0 +1,5 @@
export const queryKeys = {
base: () => ['settings'] as const,
public: () => [...queryKeys.base(), 'public'] as const,
experimental: () => [...queryKeys.base(), 'experimental'] as const,
};

View File

@ -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'
);
}
}

View File

@ -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');
}
}

View File

@ -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,
});
}

View File

@ -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')
)
);

View File

@ -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}`;

View File

@ -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;