refactor(settings): move app settings to panel [EE-5503] (#9043)

pull/9048/head
Chaim Lev-Ari 2023-06-07 12:16:47 +07:00 committed by GitHub
parent 4f04fe54a7
commit c7756f3018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 477 additions and 201 deletions

View File

@ -6,6 +6,7 @@ import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/Inte
import { r2a } from '@/react-tools/react2angular'; import { r2a } from '@/react-tools/react2angular';
import { withReactQuery } from '@/react-tools/withReactQuery'; import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter'; import { withUIRouter } from '@/react-tools/withUIRouter';
import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsView/ApplicationSettingsPanel';
export const settingsModule = angular export const settingsModule = angular
.module('portainer.app.react.components.settings', []) .module('portainer.app.react.components.settings', [])
@ -17,4 +18,8 @@ export const settingsModule = angular
.component( .component(
'internalAuth', 'internalAuth',
r2a(InternalAuth, ['onSaveSettings', 'isLoading', 'value', 'onChange']) r2a(InternalAuth, ['onSaveSettings', 'isLoading', 'value', 'onChange'])
)
.component(
'applicationSettingsPanel',
r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess'])
).name; ).name;

View File

@ -1,137 +1,6 @@
<page-header title="'Settings'" breadcrumbs="['Settings']"> </page-header> <page-header title="'Settings'" breadcrumbs="['Settings']"> </page-header>
<div class="row"> <application-settings-panel on-success="(handleSuccess)"></application-settings-panel>
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="settings" title-text="Application settings"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- snapshot-interval -->
<div class="form-group">
<label for="snapshot_interval" class="col-sm-2 control-label text-left">Snapshot interval</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="settings.SnapshotInterval" id="snapshot_interval" placeholder="e.g. 15m" />
</div>
</div>
<!-- !snapshot-interval -->
<!-- checkin-interval -->
<edge-checkin-interval-field
size="'xsmall'"
value="settings.EdgeAgentCheckinInterval"
label="'Edge agent default poll frequency'"
is-default-hidden="true"
on-change="(onChangeCheckInInterval)"
></edge-checkin-interval-field>
<!-- !checkin-interval -->
<!-- logo -->
<div class="form-group">
<por-switch-field
label="'Use custom logo'"
checked="formValues.customLogo"
name="'toggle_logo'"
disabled="state.isDemo"
on-change="(onToggleCustomLogo)"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
<div class="col-sm-12" ng-if="state.isDemo" style="margin-top: 10px">
<span class="small text-muted">You cannot use this feature in the demo version of Portainer.</span>
</div>
</div>
<div ng-if="formValues.customLogo">
<div class="form-group">
<span class="col-sm-12 text-muted small"> You can specify the URL to your logo here. For an optimal display, logo dimensions should be 155px by 55px. </span>
</div>
<div class="form-group">
<label for="logo_url" class="col-sm-2 control-label text-left"> URL </label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="settings.LogoURL" id="logo_url" placeholder="https://mycompany.com/logo.png" />
</div>
</div>
</div>
<!-- !logo -->
<div class="form-group">
<por-switch-field
label="'Allow the collection of anonymous statistics'"
checked="formValues.enableTelemetry"
name="'toggle_enableTelemetry'"
on-change="(onToggleEnableTelemetry)"
disabled="state.isDemo"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
<div class="col-sm-12" ng-if="state.isDemo" style="margin-top: 10px">
<span class="small text-muted">You cannot use this feature in the demo version of Portainer.</span>
</div>
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
You can find more information about this in our
<a href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/" target="_blank">privacy policy</a>.
</div>
</div>
<!-- login screen banner -->
<div class="form-group">
<por-switch-field
label="'Login screen banner'"
name="'toggle_login_banner'"
feature-id="customBannerFeatureId"
checked="$formValues.customLoginBanner"
on-change="(onToggleCustomLoginBanner)"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
</div>
<!-- !login screen banner -->
<!-- templates -->
<div class="col-sm-12 form-section-title"> App Templates </div>
<div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own template definitions file here. See
<a href="https://docs.portainer.io/advanced/app-templates/build" target="_blank">Portainer documentation</a> for more details.
</span>
</div>
<div class="form-group">
<label for="templates_url" class="col-sm-2 control-label text-left"> URL </label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="settings.TemplatesURL"
id="templates_url"
placeholder="https://myserver.mydomain/templates.json"
required
data-cy="settings-templateUrl"
/>
</div>
</div>
</div>
<!-- !templates -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="saveApplicationSettings()"
ng-disabled="state.actionInProgress || !settings.TemplatesURL"
button-spinner="state.actionInProgress"
data-cy="settings-saveSettingsButton"
>
<span ng-hide="state.actionInProgress">Save application settings</span>
<span ng-show="state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">

View File

@ -11,14 +11,14 @@ angular.module('portainer.app').controller('SettingsController', [
'BackupService', 'BackupService',
'FileSaver', 'FileSaver',
function ($scope, Notifications, SettingsService, StateManager, BackupService, FileSaver) { function ($scope, Notifications, SettingsService, StateManager, BackupService, FileSaver) {
$scope.customBannerFeatureId = FeatureId.CUSTOM_LOGIN_BANNER;
$scope.s3BackupFeatureId = FeatureId.S3_BACKUP_SETTING; $scope.s3BackupFeatureId = FeatureId.S3_BACKUP_SETTING;
$scope.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS; $scope.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS;
$scope.updateSettings = updateSettings;
$scope.handleSuccess = handleSuccess;
$scope.backupOptions = options; $scope.backupOptions = options;
$scope.state = { $scope.state = {
isDemo: false,
actionInProgress: false, actionInProgress: false,
availableKubeconfigExpiryOptions: [ availableKubeconfigExpiryOptions: [
{ {
@ -50,32 +50,16 @@ angular.module('portainer.app').controller('SettingsController', [
$scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' }; $scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
$scope.formValues = { $scope.formValues = {
customLogo: false,
KubeconfigExpiry: undefined, KubeconfigExpiry: undefined,
HelmRepositoryURL: undefined, HelmRepositoryURL: undefined,
BlackListedLabels: [], BlackListedLabels: [],
labelName: '', labelName: '',
labelValue: '', labelValue: '',
enableTelemetry: false,
passwordProtect: false, passwordProtect: false,
password: '', password: '',
backupFormType: $scope.BACKUP_FORM_TYPES.FILE, backupFormType: $scope.BACKUP_FORM_TYPES.FILE,
}; };
$scope.initialFormValues = {};
$scope.onToggleEnableTelemetry = function onToggleEnableTelemetry(checked) {
$scope.$evalAsync(() => {
$scope.formValues.enableTelemetry = checked;
});
};
$scope.onToggleCustomLogo = function onToggleCustomLogo(checked) {
$scope.$evalAsync(() => {
$scope.formValues.customLogo = checked;
});
};
$scope.onToggleAutoBackups = function onToggleAutoBackups(checked) { $scope.onToggleAutoBackups = function onToggleAutoBackups(checked) {
$scope.$evalAsync(() => { $scope.$evalAsync(() => {
$scope.formValues.scheduleAutomaticBackups = checked; $scope.formValues.scheduleAutomaticBackups = checked;
@ -87,13 +71,6 @@ angular.module('portainer.app').controller('SettingsController', [
$scope.state.featureLimited = limited; $scope.state.featureLimited = limited;
}; };
$scope.onChangeCheckInInterval = function (interval) {
$scope.$evalAsync(() => {
var settings = $scope.settings;
settings.EdgeAgentCheckinInterval = interval;
});
};
$scope.removeFilteredContainerLabel = function (index) { $scope.removeFilteredContainerLabel = function (index) {
const filteredSettings = $scope.formValues.BlackListedLabels.filter((_, i) => i !== index); const filteredSettings = $scope.formValues.BlackListedLabels.filter((_, i) => i !== index);
const filteredSettingsPayload = { BlackListedLabels: filteredSettings }; const filteredSettingsPayload = { BlackListedLabels: filteredSettings };
@ -133,20 +110,6 @@ angular.module('portainer.app').controller('SettingsController', [
}); });
}; };
// only update the values from the app settings widget. In future separate the api endpoints
$scope.saveApplicationSettings = function () {
const appSettingsPayload = {
SnapshotInterval: $scope.settings.SnapshotInterval,
LogoURL: $scope.formValues.customLogo ? $scope.settings.LogoURL : '',
EnableTelemetry: $scope.formValues.enableTelemetry,
TemplatesURL: $scope.settings.TemplatesURL,
EdgeAgentCheckinInterval: $scope.settings.EdgeAgentCheckinInterval,
};
$scope.state.actionInProgress = true;
updateSettings(appSettingsPayload, 'Application settings updated');
};
// only update the values from the kube settings widget. In future separate the api endpoints // only update the values from the kube settings widget. In future separate the api endpoints
$scope.saveKubernetesSettings = function () { $scope.saveKubernetesSettings = function () {
const kubeSettingsPayload = { const kubeSettingsPayload = {
@ -160,14 +123,10 @@ angular.module('portainer.app').controller('SettingsController', [
}; };
function updateSettings(settings, successMessage = 'Settings updated') { function updateSettings(settings, successMessage = 'Settings updated') {
SettingsService.update(settings) return SettingsService.update(settings)
.then(function success(response) { .then(function success(settings) {
Notifications.success('Success', successMessage); Notifications.success('Success', successMessage);
StateManager.updateLogo(settings.LogoURL); handleSuccess(settings);
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
$scope.initialFormValues.enableTelemetry = response.EnableTelemetry;
$scope.formValues.BlackListedLabels = response.BlackListedLabels;
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update settings'); Notifications.error('Failure', err, 'Unable to update settings');
@ -178,24 +137,28 @@ angular.module('portainer.app').controller('SettingsController', [
}); });
} }
function initView() { function handleSuccess(settings) {
const state = StateManager.getState(); if (settings) {
$scope.state.isDemo = state.application.demoEnvironment.enabled; StateManager.updateLogo(settings.LogoURL);
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
$scope.formValues.BlackListedLabels = settings.BlackListedLabels;
}
// trigger an event to update the deployment options for the react based sidebar
const event = new CustomEvent('portainer:deploymentOptionsUpdated');
document.dispatchEvent(event);
}
function initView() {
SettingsService.settings() SettingsService.settings()
.then(function success(data) { .then(function success(data) {
var settings = data; var settings = data;
$scope.settings = settings; $scope.settings = settings;
if (settings.LogoURL !== '') {
$scope.formValues.customLogo = true;
}
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
$scope.formValues.KubeconfigExpiry = settings.KubeconfigExpiry; $scope.formValues.KubeconfigExpiry = settings.KubeconfigExpiry;
$scope.formValues.HelmRepositoryURL = settings.HelmRepositoryURL; $scope.formValues.HelmRepositoryURL = settings.HelmRepositoryURL;
$scope.formValues.BlackListedLabels = settings.BlackListedLabels; $scope.formValues.BlackListedLabels = settings.BlackListedLabels;
$scope.initialFormValues.enableTelemetry = settings.EnableTelemetry;
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings'); Notifications.error('Failure', err, 'Unable to retrieve application settings');

View File

@ -0,0 +1,15 @@
export function isValidUrl(
value: string | undefined,
additionalCheck: (url: URL) => boolean = () => true
) {
if (!value) {
return false;
}
try {
const url = new URL(value);
return additionalCheck(url);
} catch {
return false;
}
}

View File

@ -61,6 +61,7 @@ export function EdgeCheckinIntervalField({
}} }}
options={options} options={options}
disabled={readonly} disabled={readonly}
id="edge_checkin"
/> />
</FormControl> </FormControl>
); );

View File

@ -3,6 +3,7 @@ import { string } from 'yup';
import { FormControl } from '@@/form-components/FormControl'; import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input'; import { Input } from '@@/form-components/Input';
import { isValidUrl } from '@@/form-components/validate-url';
interface Props { interface Props {
fieldName: string; fieldName: string;
@ -47,18 +48,11 @@ export function validation() {
.test( .test(
'valid API server URL', 'valid API server URL',
'The API server URL must be a valid URL (localhost cannot be used)', 'The API server URL must be a valid URL (localhost cannot be used)',
(value) => { (value) =>
if (!value) { isValidUrl(
return false; value,
} (url) => !!url.hostname && url.hostname !== 'localhost'
)
try {
const url = new URL(value);
return !!url.hostname && url.hostname !== 'localhost';
} catch {
return false;
}
}
); );
} }

View File

@ -0,0 +1,138 @@
import { Settings as SettingsIcon } from 'lucide-react';
import { Field, Form, Formik, useFormikContext } from 'formik';
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
import {
useSettings,
useUpdateSettingsMutation,
} from '@/react/portainer/settings/queries';
import { notifySuccess } from '@/portainer/services/notifications';
import { Widget } from '@@/Widget';
import { LoadingButton } from '@@/buttons';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { type Settings } from '../../types';
import { validation } from './validation';
import { Values } from './types';
import { LogoFieldset } from './LogoFieldset';
import { ScreenBannerFieldset } from './ScreenBannerFieldset';
import { TemplatesUrlSection } from './TemplatesUrlSection';
import { EnableTelemetryField } from './EnableTelemetryField';
export function ApplicationSettingsPanel({
onSuccess,
}: {
onSuccess(settings: Settings): void;
}) {
const settingsQuery = useSettings();
const mutation = useUpdateSettingsMutation();
if (!settingsQuery.data) {
return null;
}
const settings = settingsQuery.data;
const initialValues: Values = {
edgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
enableTelemetry: settings.EnableTelemetry,
loginBannerEnabled: !!settings.CustomLoginBanner,
loginBanner: settings.CustomLoginBanner,
logoEnabled: !!settings.LogoURL,
logo: settings.LogoURL,
snapshotInterval: settings.SnapshotInterval,
templatesUrl: settings.TemplatesURL,
};
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>
);
function handleSubmit(values: Values) {
mutation.mutate(
{
SnapshotInterval: values.snapshotInterval,
LogoURL: values.logo,
EnableTelemetry: values.enableTelemetry,
CustomLoginBanner: values.loginBanner,
TemplatesURL: values.templatesUrl,
EdgeAgentCheckinInterval: values.edgeAgentCheckinInterval,
},
{
onSuccess(settings) {
notifySuccess('Success', 'Application settings updated');
onSuccess(settings);
},
}
);
}
}
function InnerForm({ isLoading }: { isLoading: boolean }) {
const { values, setFieldValue, isValid, errors } = useFormikContext<Values>();
return (
<Form className="form-horizontal">
<FormControl
label="Snapshot interval"
inputId="snapshot_interval"
errors={errors.snapshotInterval}
required
>
<Field
as={Input}
id="snapshot_interval"
placeholder="e.g. 15m"
name="snapshotInterval"
/>
</FormControl>
<EdgeCheckinIntervalField
size="xsmall"
value={values.edgeAgentCheckinInterval}
label="Edge agent default poll frequency"
isDefaultHidden
onChange={(value) => setFieldValue('edgeAgentCheckinInterval', value)}
/>
<LogoFieldset />
<EnableTelemetryField />
<ScreenBannerFieldset />
<TemplatesUrlSection />
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
isLoading={isLoading}
disabled={!isValid}
data-cy="settings-saveSettingsButton"
loadingText="Saving..."
>
Save application settings
</LoadingButton>
</div>
</div>
</Form>
);
}

View File

@ -0,0 +1,16 @@
import { useIsDemo } from '@/react/portainer/system/useSystemStatus';
export function DemoAlert() {
const isDemoQuery = useIsDemo();
if (!isDemoQuery.data) {
return null;
}
return (
<div className="col-sm-12 mt-2">
<span className="small text-muted">
You cannot use this feature in the demo version of Portainer.
</span>
</div>
);
}

View File

@ -0,0 +1,41 @@
import { useField } from 'formik';
import { useIsDemo } from '@/react/portainer/system/useSystemStatus';
import { SwitchField } from '@@/form-components/SwitchField';
import { DemoAlert } from './DemoAlert';
export function EnableTelemetryField() {
const isDemoQuery = useIsDemo();
const [{ value }, , { setValue }] = useField<boolean>('enableTelemetry');
return (
<div className="form-group">
<div className="col-sm-12">
<SwitchField
labelClass="col-sm-2"
label="Allow the collection of anonymous statistics"
checked={value}
name="toggle_enableTelemetry"
onChange={(checked) => setValue(checked)}
disabled={isDemoQuery.data}
/>
</div>
<DemoAlert />
<div className="col-sm-12 text-muted small mt-2">
You can find more information about this in our{' '}
<a
href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/"
target="_blank"
rel="noreferrer"
>
privacy policy
</a>
.
</div>
</div>
);
}

View File

@ -0,0 +1,55 @@
import { useField, Field } from 'formik';
import { useIsDemo } from '@/react/portainer/system/useSystemStatus';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { SwitchField } from '@@/form-components/SwitchField';
import { useToggledValue } from './useToggledValue';
import { DemoAlert } from './DemoAlert';
export function LogoFieldset() {
const [{ name }, { error }] = useField<string>('logo');
const isDemoQuery = useIsDemo();
const [isEnabled, setIsEnabled] = useToggledValue('logo');
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
label="Use custom logo"
checked={isEnabled}
name="toggle_logo"
labelClass="col-sm-2"
disabled={isDemoQuery.data}
onChange={(checked) => setIsEnabled(checked)}
/>
</div>
<DemoAlert />
</div>
{isEnabled && (
<div>
<div className="form-group">
<span className="col-sm-12 text-muted small">
You can specify the URL to your logo here. For an optimal display,
logo dimensions should be 155px by 55px.
</span>
</div>
<FormControl label="URL" inputId="logo_url" errors={error} required>
<Field
as={Input}
name={name}
id="logo_url"
placeholder="https://mycompany.com/logo.png"
/>
</FormControl>
</div>
)}
</>
);
}

View File

@ -0,0 +1,59 @@
import { useField, Field } from 'formik';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { useIsDemo } from '@/react/portainer/system/useSystemStatus';
import { FormControl } from '@@/form-components/FormControl';
import { TextArea } from '@@/form-components/Input/Textarea';
import { SwitchField } from '@@/form-components/SwitchField';
import { useToggledValue } from './useToggledValue';
import { DemoAlert } from './DemoAlert';
export function ScreenBannerFieldset() {
const isDemoQuery = useIsDemo();
const [{ name }, { error }] = useField<string>('loginBanner');
const [isEnabled, setIsEnabled] = useToggledValue('loginBanner');
return (
<>
<div className="form-group">
<div className="col-sm-12">
<SwitchField
labelClass="col-sm-2"
label="Login screen banner"
checked={isEnabled}
name="toggle_login_banner"
disabled={isDemoQuery.data}
onChange={(checked) => setIsEnabled(checked)}
featureId={FeatureId.CUSTOM_LOGIN_BANNER}
/>
</div>
<DemoAlert />
<div className="col-sm-12 text-muted small mt-2">
You can set a custom banner that will be shown to all users during
login.
</div>
</div>
{isEnabled && (
<FormControl
label="Details"
inputId="custom_login_banner"
errors={error}
required
>
<Field
as={TextArea}
name={name}
rows="5"
id="custom_login_banner"
placeholder="Banner details"
/>
</FormControl>
)}
</>
);
}

View File

@ -0,0 +1,38 @@
import { useField, Field } from 'formik';
import { FormControl } from '@@/form-components/FormControl';
import { FormSection } from '@@/form-components/FormSection';
import { Input } from '@@/form-components/Input';
export function TemplatesUrlSection() {
const [{ name }, { error }] = useField<string>('templatesUrl');
return (
<FormSection title="App Templates">
<div className="form-group">
<span className="col-sm-12 text-muted small">
You can specify the URL to your own template definitions file here.
See{' '}
<a
href="https://docs.portainer.io/advanced/app-templates/build"
target="_blank"
rel="noreferrer"
>
Portainer documentation
</a>{' '}
for more details.
</span>
</div>
<FormControl label="URL" inputId="templates_url" required errors={error}>
<Field
as={Input}
id="templates_url"
placeholder="https://myserver.mydomain/templates.json"
required
data-cy="settings-templateUrl"
name={name}
/>
</FormControl>
</FormSection>
);
}

View File

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

View File

@ -0,0 +1,10 @@
export interface Values {
snapshotInterval: string;
edgeAgentCheckinInterval: number;
enableTelemetry: boolean;
loginBanner: string;
loginBannerEnabled: boolean;
logo: string;
logoEnabled: boolean;
templatesUrl: string;
}

View File

@ -0,0 +1,18 @@
import { useField } from 'formik';
import { useState } from 'react';
export function useToggledValue(fieldName: string) {
const [, { value }, { setValue }] = useField<string>(fieldName);
const [, { value: isEnabled }, { setValue: setIsEnabled }] =
useField<boolean>(`${fieldName}Enabled`);
const [oldValue, setOldValue] = useState(value);
async function handleIsEnabledChange(enabled: boolean) {
setOldValue(enabled ? '' : value);
// `setValue` is async, formik types are wrong for this version
await setIsEnabled(enabled);
await setValue(enabled ? oldValue : '', true);
}
return [isEnabled, handleIsEnabledChange] as const;
}

View File

@ -0,0 +1,36 @@
import { SchemaOf, bool, boolean, number, object, string } from 'yup';
import { isValidUrl } from '@@/form-components/validate-url';
import { Values } from './types';
export function validation(): SchemaOf<Values> {
return object({
edgeAgentCheckinInterval: number().required(),
enableTelemetry: bool().required(),
loginBannerEnabled: boolean().default(false),
loginBanner: string()
.default('')
.when('loginBannerEnabled', {
is: true,
then: (schema) =>
schema.required('Login banner is required when enabled'),
}),
logoEnabled: boolean().default(false),
logo: string()
.default('')
.when('logoEnabled', {
is: true,
then: (schema) =>
schema
.required('Logo url is required when enabled')
.test('valid-url', 'Must be a valid URL', (value) =>
isValidUrl(value)
),
}),
snapshotInterval: string().required('Snapshot interval is required'),
templatesUrl: string()
.required('Templates URL is required')
.test('valid-url', 'Must be a valid URL', (value) => isValidUrl(value)),
});
}

View File

@ -34,7 +34,8 @@ type OptionalSettings = Omit<Partial<Settings>, 'Edge'> & {
export async function updateSettings(settings: OptionalSettings) { export async function updateSettings(settings: OptionalSettings) {
try { try {
await axios.put(buildUrl(), settings); const { data } = await axios.put<Settings>(buildUrl(), settings);
return data;
} catch (e) { } catch (e) {
throw parseAxiosError(e as Error, 'Unable to update application settings'); throw parseAxiosError(e as Error, 'Unable to update application settings');
} }

View File

@ -95,6 +95,7 @@ export interface DefaultRegistry {
export interface Settings { export interface Settings {
LogoURL: string; LogoURL: string;
CustomLoginBanner: string;
BlackListedLabels: Pair[]; BlackListedLabels: Pair[];
AuthenticationMethod: AuthenticationMethod; AuthenticationMethod: AuthenticationMethod;
InternalAuthSettings: { RequiredPasswordLength: number }; InternalAuthSettings: { RequiredPasswordLength: number };

View File

@ -2,6 +2,10 @@ import { useQuery } from 'react-query';
import { RetryValue } from 'react-query/types/core/retryer'; import { RetryValue } from 'react-query/types/core/retryer';
import axios, { parseAxiosError } from '@/portainer/services/axios'; import axios, { parseAxiosError } from '@/portainer/services/axios';
import { UserId } from '@/portainer/users/types';
import { isBE } from '../feature-flags/feature-flags.service';
import { EnvironmentId } from '../environments/types';
import { buildUrl } from './build-url'; import { buildUrl } from './build-url';
import { queryKeys } from './query-keys'; import { queryKeys } from './query-keys';
@ -12,13 +16,18 @@ export interface StatusResponse {
Edition: string; Edition: string;
Version: string; Version: string;
InstanceID: string; InstanceID: string;
DemoEnvironment: {
Enabled: boolean;
Users: Array<UserId>;
Environments: Array<EnvironmentId>;
};
} }
export async function getSystemStatus() { export async function getSystemStatus() {
try { try {
const { data } = await axios.get<StatusResponse>(buildUrl('status')); const { data } = await axios.get<StatusResponse>(buildUrl('status'));
data.Edition = 'Community Edition'; data.Edition = isBE ? 'Business Edition' : 'Community Edition';
return data; return data;
} catch (error) { } catch (error) {
@ -45,3 +54,9 @@ export function useSystemStatus<T = StatusResponse>({
onSuccess, onSuccess,
}); });
} }
export function useIsDemo() {
return useSystemStatus({
select: (status) => status.DemoEnvironment.Enabled,
});
}