Compare commits

...

5 Commits

Author SHA1 Message Date
Chaim Lev-Ari
f2e59f3b4e refactor(settings): move buckup settign to component
fix(settings/backup): sync with CE
2023-06-06 10:51:10 +07:00
Chaim Lev-Ari
31047a5b3d refactor(settings): move hidden containers panel to component 2023-06-06 10:44:47 +07:00
Chaim Lev-Ari
4623f27bff refactor(settings): kube settings panel
fix(settings/kube): limit features
2023-06-06 10:41:26 +07:00
Chaim Lev-Ari
8f322e3c17 add comments 2023-06-06 10:29:14 +07:00
Chaim Lev-Ari
4a9236967b refactor(settings): move app settings to panel
fix(settings): fix app panel

fix(settings/apps): limit features
2023-06-06 10:29:12 +07:00
14 changed files with 1057 additions and 679 deletions

View File

@@ -0,0 +1,88 @@
import { FeatureId } from '@/react/portainer/feature-flags/enums';
/* @ngInject */
export default function ApplicationSettingsPanelController($scope, $async, StateManager) {
this.saveApplicationSettings = saveApplicationSettings.bind(this);
this.onChangeCheckInInterval = onChangeCheckInInterval.bind(this);
this.onToggleCustomLogo = onToggleCustomLogo.bind(this);
this.$onInit = $onInit.bind(this);
this.onToggleEnableTelemetry = onToggleEnableTelemetry.bind(this);
this.onToggleCustomLoginBanner = onToggleCustomLoginBanner.bind(this);
this.onChangeFormValues = onChangeFormValues.bind(this);
this.customBannerFeatureId = FeatureId.CUSTOM_LOGIN_BANNER;
this.formValues = {
logoURL: '',
logoEnabled: false,
customLoginBannerEnabled: false,
customLoginBanner: '',
snapshotInterval: '',
enableTelemetry: false,
templatesUrl: '',
edgeAgentCheckinInterval: '',
};
this.state = {
isDemo: false,
actionInProgress: false,
};
async function saveApplicationSettings() {
$async(async () => {
const appSettingsPayload = {
SnapshotInterval: this.settings.SnapshotInterval,
LogoURL: this.formValues.customLogo ? this.settings.LogoURL : '',
EnableTelemetry: this.settings.EnableTelemetry,
CustomLoginBanner: this.formValues.customLoginBanner ? this.settings.CustomLoginBanner : '',
TemplatesURL: this.settings.TemplatesURL,
EdgeAgentCheckinInterval: this.settings.EdgeAgentCheckinInterval,
};
this.state.actionInProgress = true;
await this.onSubmit(appSettingsPayload, 'Application settings updated');
this.state.actionInProgress = false;
});
}
function onToggleCustomLogo(logoEnabled) {
this.onChangeFormValues({ logoEnabled });
}
function onChangeCheckInInterval(edgeAgentCheckinInterval) {
this.onChangeFormValues({ edgeAgentCheckinInterval });
}
function onToggleEnableTelemetry(enableTelemetry) {
this.onChangeFormValues({ enableTelemetry });
}
function onToggleCustomLoginBanner(customLoginBannerEnabled) {
this.onChangeFormValues({ customLoginBannerEnabled });
}
function onChangeFormValues(newPartialValues) {
$scope.$evalAsync(() => {
this.formValues = {
...this.formValues,
...newPartialValues,
};
});
}
function $onInit() {
const state = StateManager.getState();
this.state.isDemo = state.application.demoEnvironment.enabled;
this.formValues = {
logoURL: this.settings.LogoURL,
logoEnabled: !!this.settings.LogoURL,
customLoginBannerEnabled: !!this.settings.CustomLoginBanner,
customLoginBanner: this.settings.CustomLoginBanner,
snapshotInterval: this.settings.SnapshotInterval,
enableTelemetry: this.settings.EnableTelemetry,
templatesUrl: this.settings.TemplatesURL,
edgeAgentCheckinInterval: this.settings.EdgeAgentCheckinInterval,
};
}
}

View File

@@ -0,0 +1,148 @@
<div class="row">
<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="$ctrl.formValues.snapshotInterval" id="snapshot_interval" placeholder="e.g. 15m" />
</div>
</div>
<!-- !snapshot-interval -->
<!-- checkin-interval -->
<edge-checkin-interval-field
size="'xsmall'"
value="$ctrl.formValues.edgeAgentCheckinInterval"
label="'Edge agent default poll frequency'"
is-default-hidden="true"
on-change="($ctrl.onChangeCheckInInterval)"
></edge-checkin-interval-field>
<!-- !checkin-interval -->
<!-- logo -->
<div class="form-group">
<por-switch-field
label="'Use custom logo'"
checked="$ctrl.formValues.logoEnabled"
name="'toggle_logo'"
disabled="$ctrl.state.isDemo"
on-change="($ctrl.onToggleCustomLogo)"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
<div class="col-sm-12" ng-if="$ctrl.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="$ctrl.formValues.logoEnabled">
<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="$ctrl.formValues.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="$ctrl.formValues.enableTelemetry"
name="'toggle_enableTelemetry'"
on-change="($ctrl.onToggleEnableTelemetry)"
disabled="$ctrl.state.isDemo"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
<div class="col-sm-12" ng-if="$ctrl.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'"
checked="$ctrl.formValues.customLoginBannerEnabled"
name="'toggle_login_banner'"
disabled="$ctrl.state.isDemo"
feature-id="$ctrl.customBannerFeatureId"
on-change="($ctrl.onToggleCustomLoginBanner)"
field-class="'col-sm-12'"
label-class="'col-sm-2'"
></por-switch-field>
<div class="col-sm-12" ng-if="$ctrl.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 set a custom banner that will be shown to all users during login. </div>
</div>
<div ng-if="$ctrl.formValues.customLoginBannerEnabled">
<div class="form-group">
<label for="custom_login_banner" class="col-sm-2 control-label required text-left">Details</label>
<div class="col-sm-10">
<textarea class="form-control" rows="5" ng-model="$ctrl.formValues.customLoginBanner" id="custom_login_banner" placeholder="Banner details"></textarea>
</div>
</div>
</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="$ctrl.formValues.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 !ml-0"
ng-click="$ctrl.saveApplicationSettings()"
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.templatesUrl || ($ctrl.formValues.customLoginBannerEnabled && !$ctrl.formValues.customLoginBanner)"
button-spinner="$ctrl.state.actionInProgress"
data-cy="settings-saveSettingsButton"
>
<span ng-hide="$ctrl.state.actionInProgress">Save application settings</span>
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import angular from 'angular';
import controller from './application-settings-panel.controller';
angular.module('portainer.app').component('applicationSettingsPanel', {
templateUrl: './application-settings-panel.html',
controller,
bindings: {
settings: '<',
onSubmit: '<',
},
});

View File

@@ -0,0 +1,131 @@
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { options } from '@/react/portainer/settings/SettingsView/backup-options';
/* @ngInject */
export default function BackupSettingsPanelController($scope, $async, BackupService, Notifications, FileSaver) {
this.$onInit = $onInit.bind(this);
this.onBackupOptionsChange = onBackupOptionsChange.bind(this);
this.onToggleAutoBackups = onToggleAutoBackups.bind(this);
this.getS3SettingsPayload = getS3SettingsPayload.bind(this);
this.downloadBackup = downloadBackup.bind(this);
this.saveS3BackupSettings = saveS3BackupSettings.bind(this);
this.exportBackup = exportBackup.bind(this);
this.backupOptions = options;
this.s3BackupFeatureId = FeatureId.S3_BACKUP_SETTING;
this.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
this.formValues = {
passwordProtect: false,
password: '',
scheduleAutomaticBackups: true,
cronRule: '',
accessKeyId: '',
secretAccessKey: '',
region: '',
bucketName: '',
s3CompatibleHost: '',
backupFormType: this.BACKUP_FORM_TYPES.FILE,
};
this.state = {
backupInProgress: false,
featureLimited: false,
};
function onToggleAutoBackups(checked) {
$scope.$evalAsync(() => {
this.formValues.scheduleAutomaticBackups = checked;
});
}
function onBackupOptionsChange(type, limited) {
this.formValues.backupFormType = type;
this.state.featureLimited = limited;
}
function downloadBackup() {
const payload = {};
if (this.formValues.passwordProtect) {
payload.password = this.formValues.password;
}
this.state.backupInProgress = true;
BackupService.downloadBackup(payload)
.then((data) => {
const downloadData = new Blob([data.file], { type: 'application/gzip' });
FileSaver.saveAs(downloadData, data.name);
Notifications.success('Success', 'Backup successfully downloaded');
})
.catch((err) => {
Notifications.error('Failure', err, 'Unable to download backup');
})
.finally(() => {
this.state.backupInProgress = false;
});
}
function saveS3BackupSettings() {
const payload = this.getS3SettingsPayload();
BackupService.saveS3Settings(payload)
.then(() => {
Notifications.success('Success', 'S3 Backup settings successfully saved');
})
.catch((err) => {
Notifications.error('Failure', err, 'Unable to save S3 backup settings');
})
.finally(() => {
this.state.backupInProgress = false;
});
}
function exportBackup() {
const payload = this.getS3SettingsPayload();
BackupService.exportBackup(payload)
.then(() => {
Notifications.success('Success', 'Exported backup to S3 successfully');
})
.catch((err) => {
Notifications.error('Failure', err, 'Unable to export backup to S3');
})
.finally(() => {
this.state.backupInProgress = false;
});
}
function getS3SettingsPayload() {
return {
Password: this.formValues.passwordProtectS3 ? this.formValues.passwordS3 : '',
CronRule: this.formValues.scheduleAutomaticBackups ? this.formValues.cronRule : '',
AccessKeyID: this.formValues.accessKeyId,
SecretAccessKey: this.formValues.secretAccessKey,
Region: this.formValues.region,
BucketName: this.formValues.bucketName,
S3CompatibleHost: this.formValues.s3CompatibleHost,
};
}
function $onInit() {
return $async(async () => {
try {
const data = isBE ? await BackupService.getS3Settings() : {};
this.formValues.passwordS3 = data.Password;
this.formValues.cronRule = data.CronRule;
this.formValues.accessKeyId = data.AccessKeyID;
this.formValues.secretAccessKey = data.SecretAccessKey;
this.formValues.region = data.Region;
this.formValues.bucketName = data.BucketName;
this.formValues.s3CompatibleHost = data.S3CompatibleHost;
this.formValues.scheduleAutomaticBackups = this.formValues.cronRule ? true : false;
this.formValues.passwordProtectS3 = this.formValues.passwordS3 ? true : false;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve S3 backup settings');
}
});
}
}

View File

@@ -0,0 +1,320 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="download" title-text="Back up Portainer"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" ng-submit="$ctrl.backupPortainer()" name="$ctrl.backupPortainerForm">
<div class="col-sm-12 form-section-title"> Backup configuration </div>
<div class="text-muted small mb-3">This will back up your Portainer server configuration and does not include containers.</div>
<box-selector
slim="true"
options="$ctrl.backupOptions"
value="$ctrl.formValues.backupFormType"
on-change="($ctrl.onBackupOptionsChange)"
radio-name="'backupOptions'"
></box-selector>
<div ng-if="$ctrl.formValues.backupFormType === $ctrl.BACKUP_FORM_TYPES.S3">
<!-- Schedule automatic backups -->
<div class="form-group mt-3">
<por-switch-field
label="'Schedule automatic backups'"
name="'s3-backup-setting'"
feature-id="s3BackupFeatureId"
checked="$ctrl.formValues.scheduleAutomaticBackups"
field-class="'col-sm-10'"
label-class="'col-sm-2'"
on-change="($ctrl.onToggleAutoBackups)"
></por-switch-field>
</div>
<!-- !Schedule automatic backups -->
<!-- Cron rule -->
<div class="form-group" ng-if="$ctrl.formValues.scheduleAutomaticBackups">
<label for="cron_rule" class="col-sm-2 control-label text-left">Cron rule</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
placeholder="0 2 * * *"
id="cron_rule"
name="cron_rule"
ng-model="$ctrl.formValues.cronRule"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
required
cronRule
data-cy="settings-backupCronRuleInput"
/>
</div>
</div>
<!-- !Cron rule -->
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.cron_rule.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.cron_rule.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
<p ng-message="invalidCron">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
Please enter a valid cron rule.</p
>
</div>
</div>
</div>
<!-- Access key id -->
<div class="form-group">
<label for="access_key_id" class="col-sm-2 control-label text-left">Access key ID</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="access_key_id"
name="access_key_id"
ng-model="$ctrl.formValues.accessKeyId"
ng-required="$ctrl.formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-accessKeyIdInput"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.access_key_id.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.access_key_id.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Access key id -->
<!-- Secret access key -->
<div class="form-group">
<label for="secret_access_key" class="col-sm-2 control-label text-left">Secret access key</label>
<div class="col-sm-10">
<input
type="password"
class="form-control"
id="secret_access_key"
name="secret_access_key"
ng-model="$ctrl.formValues.secretAccessKey"
ng-required="$ctrl.formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-secretAccessKeyInput"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.secret_access_key.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.secret_access_key.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Secret access key -->
<!-- Region -->
<div class="form-group">
<label for="region" class="col-sm-2 control-label text-left">Region</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
placeholder="default region is us-east-1 if left empty"
id="region"
name="region"
ng-model="$ctrl.formValues.region"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-backupRegionInput"
/>
</div>
</div>
<!-- !Region -->
<!-- Bucket name -->
<div class="form-group">
<label for="bucket_name" class="col-sm-2 control-label text-left">Bucket name</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="bucket_name"
name="bucket_name"
ng-model="$ctrl.formValues.bucketName"
ng-required="$ctrl.formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-backupBucketNameInput"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.bucket_name.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.bucket_name.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Bucket name -->
<!-- S3 compatible host -->
<div class="form-group">
<label for="s3_compatible_host" class="col-sm-2 control-label text-left">
S3 compatible host
<portainer-tooltip message="'Hostname of a S3 service'"></portainer-tooltip>
</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
placeholder="leave empty for AWS S3"
id="s3_compatible_host"
name="s3_compatible_host"
ng-model="$ctrl.formValues.s3CompatibleHost"
ng-pattern="/^https?://.+/"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-class="limited-be"
data-cy="settings-backupS3CompatibleHostInput"
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.s3_compatible_host.$invalid">
<div class="small text-warning">
<p class="vertical-center">
<pr-icon icon="'alert-triangle'"></pr-icon>
<span>S3 host must begin with http:// or https://</span>
</p>
</div>
</div>
<!-- !S3 compatible host -->
<div class="col-sm-12 form-section-title"> Security settings </div>
<!-- Password protect S3 -->
<div class="form-group">
<label for="password_protect" class="col-sm-2 control-label text-left">Password protect</label>
<div class="col-sm-10">
<label class="switch">
<input
type="checkbox"
id="password_protect_s3"
name="password_protect_s3"
ng-model="$ctrl.formValues.passwordProtectS3"
data-cy="settings-passwordProtectToggleS3"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
/><span class="slider round"></span>
</label>
</div>
</div>
<!-- !Password protect S3 -->
<!-- Password S3 -->
<div class="form-group" ng-if="$ctrl.formValues.passwordProtectS3">
<label for="password" class="col-sm-2 control-label text-left">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" ng-model="$ctrl.formValues.passwordS3" id="password_S3" name="password_S3" required data-cy="settings-backups3pw" />
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.password_S3.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.password_S3.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Password S3 -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.backupPortainerForm.$invalid"
ng-click="$ctrl.exportBackup()"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-exportBackupS3Button"
>
<span>
<pr-icon icon="'upload'" class-name="'mr-1'"></pr-icon>
Export backup
</span>
</button>
</div>
</div>
<div class="form-group">
<hr />
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.backupPortainerForm.$invalid || $ctrl.state.backupInProgress"
ng-click="$ctrl.saveS3BackupSettings()"
limited-feature-dir="{{::$ctrl.s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
data-cy="settings-saveBackupSettingsButton"
>
<span>Save backup settings</span>
</button>
</div>
</div>
</div>
<div ng-if="$ctrl.formValues.backupFormType === $ctrl.BACKUP_FORM_TYPES.FILE">
<div class="col-sm-12 form-section-title"> Security settings </div>
<!-- Password protect -->
<div class="form-group">
<label for="password_protect" class="col-sm-2 control-label text-left">Password protect</label>
<div class="col-sm-2">
<label class="switch" data-cy="settings-passwordProtectLocal">
<input type="checkbox" id="password_protect" name="password_protect" ng-model="$ctrl.formValues.passwordProtect" />
<span class="slider round"></span>
</label>
</div>
</div>
<!-- !Password protect -->
<!-- Password -->
<div class="form-group" ng-if="$ctrl.formValues.passwordProtect">
<label for="password" class="col-sm-2 control-label text-left">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" ng-model="$ctrl.formValues.password" id="password" name="password" required data-cy="settings-backupLocalPassword" />
</div>
</div>
<div class="form-group col-md-12" ng-show="$ctrl.backupPortainerForm.password.$invalid">
<div class="small text-warning">
<div ng-messages="$ctrl.backupPortainerForm.password.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Password -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-click="$ctrl.downloadBackup()"
ng-disabled="$ctrl.backupPortainerForm.$invalid || $ctrl.state.backupInProgress || $ctrl.state.featureLimited"
button-spinner="$ctrl.state.backupInProgress"
data-cy="settings-downloadLocalBackup"
>
<span ng-hide="$ctrl.state.backupInProgress">
<pr-icon icon="'download'"></pr-icon>
Download backup
</span>
<span ng-show="$ctrl.state.backupInProgress">
<pr-icon icon="'download'"></pr-icon>
Downloading backup
</span>
</button>
</div>
</div>
<!-- !actions -->
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import angular from 'angular';
import controller from './backup-settings-panel.controller';
angular.module('portainer.app').component('backupSettingsPanel', {
templateUrl: './backup-settings-panel.html',
controller,
bindings: {
settings: '<',
onSubmit: '<',
},
});

View File

@@ -0,0 +1,51 @@
/* @ngInject */
export default function HiddenContainersPanelController($async) {
this.addFilteredContainerLabel = addFilteredContainerLabel.bind(this);
this.removeFilteredContainerLabel = removeFilteredContainerLabel.bind(this);
this.$onInit = $onInit.bind(this);
this.handleSubmit = handleSubmit.bind(this);
this.state = {
actionInProgress: false,
};
this.formValues = {
BlackListedLabels: [],
labelName: '',
labelValue: '',
};
this.state = {
actionInProgress: false,
};
function addFilteredContainerLabel() {
const label = {
name: this.formValues.labelName,
value: this.formValues.labelValue,
};
const filteredSettings = [...this.formValues.BlackListedLabels, label];
this.handleSubmit(filteredSettings);
this.formValues.labelName = '';
this.formValues.labelValue = '';
}
function removeFilteredContainerLabel(index) {
const filteredSettings = this.formValues.BlackListedLabels.filter((item, i) => i !== index);
this.handleSubmit(filteredSettings);
}
function handleSubmit(labels) {
return $async(async () => {
this.state.actionInProgress = true;
await this.onSubmit({ BlackListedLabels: labels }, 'Hidden container settings updated');
this.formValues.BlackListedLabels = labels;
this.state.actionInProgress = false;
});
}
function $onInit() {
this.formValues.BlackListedLabels = this.settings.BlackListedLabels;
}
}

View File

@@ -0,0 +1,61 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="box" title-text="Hidden containers"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" ng-submit="$ctrl.addFilteredContainerLabel()" name="addTagForm">
<div class="form-group">
<span class="col-sm-12 text-muted small"> You can hide containers with specific labels from Portainer UI. You need to specify the label name and value. </span>
</div>
<div class="form-group">
<label for="header_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-4">
<input type="text" required class="form-control" id="header_name" name="label_name" ng-model="$ctrl.formValues.labelName" placeholder="e.g. com.example.foo" />
</div>
<label for="header_value" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11 col-md-4">
<input type="text" class="form-control" id="header_value" ng-model="$ctrl.formValues.labelValue" placeholder="e.g. bar" />
</div>
<div class="col-sm-12 col-md-2">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.labelName"
><pr-icon icon="'plus'" class-name="'mr-1'"></pr-icon>Add filter</button
>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 table-responsive">
<table class="table-hover table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in $ctrl.formValues.BlackListedLabels">
<td>{{ label.name }}</td>
<td>{{ label.value }}</td>
<td>
<button type="button" ng-disabled="$ctrl.state.actionInProgress" class="btn btn-danger btn-xs" ng-click="$ctrl.removeFilteredContainerLabel($index)">
<pr-icon icon="'trash-2'" class-name="'mr-1'"></pr-icon>
Remove
</button>
</td>
</tr>
<tr ng-if="$ctrl.formValues.BlackListedLabels.length === 0">
<td colspan="3" class="text-muted text-center">No filter available.</td>
</tr>
<tr ng-if="!$ctrl.formValues.BlackListedLabels">
<td colspan="3" class="text-muted text-center">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !filtered-labels -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -0,0 +1,12 @@
import angular from 'angular';
import controller from './hidden-containers-panel.controller';
angular.module('portainer.app').component('hiddenContainersPanel', {
templateUrl: './hidden-containers-panel.html',
controller,
bindings: {
settings: '<',
onSubmit: '<',
},
});

View File

@@ -0,0 +1,12 @@
import angular from 'angular';
import controller from './kube-settings-panel.controller';
angular.module('portainer.app').component('kubeSettingsPanel', {
templateUrl: './kube-settings-panel.html',
controller,
bindings: {
settings: '<',
onSubmit: '<',
},
});

View File

@@ -0,0 +1,92 @@
import { FeatureId } from '@/react/portainer/feature-flags/enums';
/* @ngInject */
export default function KubeSettingsPanelController($scope, $async) {
this.onToggleAddWithForm = onToggleAddWithForm.bind(this);
this.onToggleHideWebEditor = onToggleHideWebEditor.bind(this);
this.onToggleHideFileUpload = onToggleHideFileUpload.bind(this);
this.onTogglePerEnvOverride = onTogglePerEnvOverride.bind(this);
this.saveKubernetesSettings = saveKubernetesSettings.bind(this);
this.$onInit = $onInit.bind(this);
this.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS;
this.state = {
availableKubeconfigExpiryOptions: [
{
key: '1 day',
value: '24h',
},
{
key: '7 days',
value: `${24 * 7}h`,
},
{
key: '30 days',
value: `${24 * 30}h`,
},
{
key: '1 year',
value: `${24 * 30 * 12}h`,
},
{
key: 'No expiry',
value: '0',
},
],
actionInProgress: false,
};
this.formValues = {
KubeconfigExpiry: undefined,
HelmRepositoryURL: undefined,
GlobalDeploymentOptions: {
hideAddWithForm: false,
perEnvOverride: false,
hideWebEditor: false,
hideFileUpload: false,
},
};
function onToggleAddWithForm(checked) {
$scope.$evalAsync(() => {
this.formValues.GlobalDeploymentOptions.hideAddWithForm = checked;
this.formValues.GlobalDeploymentOptions.hideWebEditor = checked;
this.formValues.GlobalDeploymentOptions.hideFileUpload = checked;
});
}
function onToggleHideWebEditor(checked) {
$scope.$evalAsync(() => {
this.formValues.GlobalDeploymentOptions.hideWebEditor = !checked;
});
}
function onToggleHideFileUpload(checked) {
$scope.$evalAsync(() => {
this.formValues.GlobalDeploymentOptions.hideFileUpload = !checked;
});
}
function onTogglePerEnvOverride(checked) {
$scope.$evalAsync(() => {
this.formValues.GlobalDeploymentOptions.perEnvOverride = checked;
});
}
async function saveKubernetesSettings() {
$async(async () => {
this.state.actionInProgress = true;
await this.onSubmit(this.formValues, 'Kubernetes settings updated');
this.state.actionInProgress = false;
});
}
function $onInit() {
if (this.settings.GlobalDeploymentOptions) {
this.formValues.GlobalDeploymentOptions = this.settings.GlobalDeploymentOptions;
}
this.formValues.KubeconfigExpiry = this.settings.KubeconfigExpiry;
this.formValues.HelmRepositoryURL = this.settings.HelmRepositoryURL;
}
}

View File

@@ -0,0 +1,103 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="svg-kube" title-text="Kubernetes settings"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- helm charts -->
<div class="col-sm-12 form-section-title"> Helm Repository </div>
<div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own helm repository here. See the
<a href="https://helm.sh/docs/topics/chart_repository/" target="_blank">official documentation</a> for more details.
</span>
</div>
<div class="form-group">
<label for="helmrepository_url" class="col-sm-2 control-label text-left"> URL </label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.formValues.HelmRepositoryURL" id="helmrepository_url" placeholder="https://charts.bitnami.com/bitnami" />
</div>
</div>
</div>
<!-- !helm charts -->
<!-- kubeconfig -->
<div class="col-sm-12 form-section-title"> Kubeconfig </div>
<div class="form-group">
<label for="kubeconfig_expiry" class="col-sm-2 control-label text-left"> Kubeconfig expiry </label>
<div class="col-sm-10">
<select
id="kubeconfig_expiry"
class="form-control"
ng-model="$ctrl.formValues.KubeconfigExpiry"
ng-options="opt.value as opt.key for opt in $ctrl.state.availableKubeconfigExpiryOptions"
></select>
</div>
</div>
<!-- !kubeconfig -->
<!-- deployment options -->
<div class="col-sm-12 form-section-title"> Deployment Options </div>
<div class="form-group">
<por-switch-field
label="'Enforce code-based deployment'"
checked="$ctrl.formValues.GlobalDeploymentOptions.hideAddWithForm"
name="'toggle_hideAddWithForm'"
feature-id="$ctrl.enforceDeploymentOptions"
on-change="($ctrl.onToggleAddWithForm)"
field-class="'col-sm-10'"
label-class="'col-sm-2'"
tooltip="'Hides the \'Add with form\' buttons and prevents adding/editing of resources via forms'"
></por-switch-field>
</div>
<div class="form-group flex flex-col gap-y-1" ng-if="$ctrl.formValues.GlobalDeploymentOptions.hideAddWithForm">
<por-switch-field
label="'Allow web editor and custom template use'"
checked="!$ctrl.formValues.GlobalDeploymentOptions.hideWebEditor"
name="'toggle_hideWebEditor'"
on-change="($ctrl.onToggleHideWebEditor)"
field-class="'col-sm-10'"
label-class="'col-sm-2 !pl-4'"
></por-switch-field>
<por-switch-field
label="'Allow specifying of a manifest via a URL'"
checked="!$ctrl.formValues.GlobalDeploymentOptions.hideFileUpload"
name="'toggle_hideFileUpload'"
on-change="($ctrl.onToggleHideFileUpload)"
field-class="'col-sm-10'"
label-class="'col-sm-2 !pl-4'"
></por-switch-field>
</div>
<div class="form-group" limited-feature-dir="{{::$ctrl.enforceDeploymentOptions}}" limited-feature-class="hidden">
<por-switch-field
label="'Allow per environment override'"
checked="$ctrl.formValues.GlobalDeploymentOptions.perEnvOverride"
name="'toggle_perEnvOverride'"
on-change="($ctrl.onTogglePerEnvOverride)"
field-class="'col-sm-10'"
label-class="'col-sm-2'"
tooltip="'Allows overriding of deployment options in the Cluster setup screen of each environment'"
></por-switch-field>
</div>
<!-- ! deployment options -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm !ml-0"
ng-click="$ctrl.saveKubernetesSettings()"
ng-disabled="$ctrl.state.actionInProgress"
button-spinner="$ctrl.state.actionInProgress"
data-cy="settings-saveKubeSettingsButton"
>
<span ng-hide="$ctrl.state.actionInProgress">Save Kubernetes settings</span>
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,511 +1,13 @@
<page-header title="'Settings'" breadcrumbs="['Settings']"> </page-header>
<div class="row">
<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 -->
<application-settings-panel ng-if="settings" settings="settings" on-submit="(updateSettings)"></application-settings-panel>
<!-- 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="col-sm-12">
<rd-widget>
<rd-widget-header icon="svg-kube" title-text="Kubernetes settings"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- helm charts -->
<div class="col-sm-12 form-section-title"> Helm Repository </div>
<div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own helm repository here. See the
<a href="https://helm.sh/docs/topics/chart_repository/" target="_blank">official documentation</a> for more details.
</span>
</div>
<div class="form-group">
<label for="helmrepository_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.HelmRepositoryURL" id="helmrepository_url" placeholder="https://charts.bitnami.com/bitnami" />
</div>
</div>
</div>
<!-- !helm charts -->
<!-- kube -->
<div class="col-sm-12 form-section-title"> Kubeconfig </div>
<div class="form-group">
<label for="kubeconfig_expiry" class="col-sm-2 control-label text-left"> Kubeconfig expiry </label>
<div class="col-sm-10">
<select
id="kubeconfig_expiry"
class="form-control"
ng-model="formValues.KubeconfigExpiry"
ng-options="opt.value as opt.key for opt in state.availableKubeconfigExpiryOptions"
></select>
</div>
</div>
<!-- ! kube -->
<!-- deployment options -->
<div class="col-sm-12 form-section-title"> Deployment Options </div>
<div class="form-group">
<por-switch-field
label="'Enforce code-based deployment'"
name="'toggle_hideAddWithForm'"
feature-id="enforceDeploymentOptions"
disabled="true"
checked="false"
field-class="'col-sm-10'"
label-class="'col-sm-2'"
tooltip="'Hides the \'Add with form\' buttons and prevents adding/editing of resources via forms'"
></por-switch-field>
</div>
<!-- !deployment options -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="saveKubernetesSettings()"
ng-disabled="state.kubeSettingsActionInProgress"
button-spinner="state.kubeSettingsActionInProgress"
data-cy="settings-saveKubeSettingsButton"
>
<span ng-hide="state.kubeSettingsActionInProgress">Save Kubernetes settings</span>
<span ng-show="state.kubeSettingsActionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<kube-settings-panel ng-if="settings" settings="settings" on-submit="(updateSettings)"></kube-settings-panel>
<ssl-ca-file-settings></ssl-ca-file-settings>
<ssl-certificate-settings ng-show="state.showHTTPS"></ssl-certificate-settings>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="box" title-text="Hidden containers"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" ng-submit="addFilteredContainerLabel()" name="addTagForm">
<div class="form-group">
<span class="col-sm-12 text-muted small"> You can hide containers with specific labels from Portainer UI. You need to specify the label name and value. </span>
</div>
<div class="form-group">
<label for="header_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-4">
<input type="text" required class="form-control" id="header_name" name="label_name" ng-model="formValues.labelName" placeholder="e.g. com.example.foo" />
</div>
<label for="header_value" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11 col-md-4">
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar" />
</div>
<div class="col-sm-12 col-md-2">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"><pr-icon icon="'plus'" class-name="'mr-1'"></pr-icon>Add filter</button>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 table-responsive">
<table class="table-hover table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in formValues.BlackListedLabels">
<td>{{ label.name }}</td>
<td>{{ label.value }}</td>
<td>
<button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)">
<pr-icon icon="'trash-2'" class-name="'mr-1'"></pr-icon>
Remove</button
>
</td>
</tr>
<tr ng-if="formValues.BlackListedLabels.length === 0">
<td colspan="3" class="text-muted text-center">No filter available.</td>
</tr>
<tr ng-if="!formValues.BlackListedLabels">
<td colspan="3" class="text-muted text-center">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !filtered-labels -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<hidden-containers-panel ng-if="settings" settings="settings" on-submit="(updateSettings)"></hidden-containers-panel>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="download" title-text="Back up Portainer"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" ng-submit="backupPortainer()" name="backupPortainerForm">
<div class="col-sm-12 form-section-title"> Backup configuration </div>
<div class="text-muted small mb-3">This will back up your Portainer server configuration and does not include containers.</div>
<box-selector slim="true" options="backupOptions" value="formValues.backupFormType" on-change="(onBackupOptionsChange)" radio-name="'backupOptions'"></box-selector>
<div ng-if="formValues.backupFormType === BACKUP_FORM_TYPES.S3">
<!-- Schedule automatic backups -->
<div class="form-group mt-3">
<por-switch-field
label="'Schedule automatic backups'"
name="'s3-backup-setting'"
feature-id="s3BackupFeatureId"
checked="formValues.scheduleAutomaticBackups"
field-class="'col-sm-10'"
label-class="'col-sm-2'"
on-change="(onToggleAutoBackups)"
></por-switch-field>
</div>
<!-- !Schedule automatic backups -->
<!-- Cron rule -->
<div class="form-group" ng-if="formValues.scheduleAutomaticBackups">
<label for="cron_rule" class="col-sm-2 control-label text-left">Cron rule</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
placeholder="0 2 * * *"
id="cron_rule"
name="cron_rule"
ng-model="formValues.cronRule"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
required
cronRule
/>
</div>
</div>
<!-- !Cron rule -->
<!-- Access key id -->
<div class="form-group">
<label for="access_key_id" class="col-sm-2 control-label text-left">Access Key ID</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="access_key_id"
name="access_key_id"
ng-model="formValues.accessKeyId"
ng-required="formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
/>
</div>
</div>
<!-- !Access key id -->
<!-- Secret access key -->
<div class="form-group">
<label for="secret_access_key" class="col-sm-2 control-label text-left">Secret Access Key</label>
<div class="col-sm-10">
<input
type="password"
class="form-control"
id="secret_access_key"
name="secret_access_key"
ng-model="formValues.secretAccessKey"
ng-required="formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
/>
</div>
</div>
<!-- !Secret access key -->
<!-- Region -->
<div class="form-group">
<label for="region" class="col-sm-2 control-label text-left">Region</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
placeholder="default region is us-east-1 if left empty"
id="region"
name="region"
ng-model="formValues.region"
ng-required="formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
/>
</div>
</div>
<!-- !Region -->
<!-- Bucket name -->
<div class="form-group">
<label for="bucket_name" class="col-sm-2 control-label text-left">Bucket name</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
id="bucket_name"
name="bucket_name"
ng-model="formValues.bucketName"
ng-required="formValues.scheduleAutomaticBackups"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
/>
</div>
</div>
<!-- !Bucket name -->
<div class="col-sm-12 form-section-title"> Security settings </div>
<!-- Password protect S3 -->
<div class="form-group">
<label for="password_protect" class="col-sm-2 control-label text-left">Password protect</label>
<div class="col-sm-10">
<label class="switch">
<input
type="checkbox"
id="password_protect_s3"
name="password_protect_s3"
ng-model="formValues.passwordProtectS3"
data-cy="settings-passwordProtectToggleS3"
disabled
/><span class="slider round"></span>
</label>
</div>
</div>
<!-- !Password protect S3 -->
<!-- Password S3 -->
<div class="form-group" ng-if="formValues.passwordProtectS3">
<label for="password" class="col-sm-2 control-label text-left">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" ng-model="formValues.passwordS3" id="password_S3" name="password_S3" required data-cy="settings-backups3pw" />
</div>
</div>
<div class="form-group col-md-12" ng-show="backupPortainerForm.password_S3.$invalid">
<div class="small text-warning">
<div ng-messages="backupPortainerForm.password_S3.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Password S3 -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="backupPortainerForm.$invalid"
ng-click="exportBackup()"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
>
<span>
<pr-icon icon="'upload'" class-name="'mr-1'"></pr-icon>
Export backup
</span>
</button>
</div>
</div>
<div class="form-group">
<hr />
<div class="col-sm-12">
<button
type="submit"
class="btn btn-primary btn-sm"
ng-disabled="backupPortainerForm.$invalid ||state.backupInProgress"
ng-click="saveS3BackupSettings()"
limited-feature-dir="{{::s3BackupFeatureId}}"
limited-feature-disabled
limited-feature-class="limited-be"
>
<span>Save backup settings</span>
</button>
</div>
</div>
</div>
<div ng-if="formValues.backupFormType === BACKUP_FORM_TYPES.FILE">
<div class="col-sm-12 form-section-title"> Security settings </div>
<!-- Password protect -->
<div class="form-group">
<label for="password_protect" class="col-sm-2 control-label text-left">Password protect</label>
<div class="col-sm-2">
<label class="switch" data-cy="settings-passwordProtectLocal">
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><span class="slider round"></span>
</label>
</div>
</div>
<!-- !Password protect -->
<!-- Password -->
<div class="form-group" ng-if="formValues.passwordProtect">
<label for="password" class="col-sm-2 control-label text-left">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required data-cy="settings-backupLocalPassword" />
</div>
</div>
<div class="form-group col-md-12" ng-show="backupPortainerForm.password.$invalid">
<div class="small text-warning">
<div ng-messages="backupPortainerForm.password.$error">
<p ng-message="required"> <pr-icon icon="'alert-triangle'"></pr-icon> This field is required.</p>
</div>
</div>
</div>
<!-- !Password -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="downloadBackup()"
ng-disabled="backupPortainerForm.$invalid || state.backupInProgress || state.featureLimited"
button-spinner="state.backupInProgress"
data-cy="settings-downloadLocalBackup"
>
<span ng-hide="state.backupInProgress">Download backup</span>
<span ng-show="state.backupInProgress">Downloading backup</span>
</button>
</div>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<!-- backup -->
<backup-settings-panel ng-if="settings" settings="settings" on-submit="(updateSettings)"></backup-settings-panel>

View File

@@ -1,201 +1,35 @@
import angular from 'angular';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { options } from '@/react/portainer/settings/SettingsView/backup-options';
angular.module('portainer.app').controller('SettingsController', [
'$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.enforceDeploymentOptions = FeatureId.ENFORCE_DEPLOYMENT_OPTIONS;
$scope.backupOptions = options;
$scope.state = {
isDemo: false,
actionInProgress: false,
availableKubeconfigExpiryOptions: [
{
key: '1 day',
value: '24h',
},
{
key: '7 days',
value: `${24 * 7}h`,
},
{
key: '30 days',
value: `${24 * 30}h`,
},
{
key: '1 year',
value: `${24 * 30 * 12}h`,
},
{
key: 'No expiry',
value: '0',
},
],
backupInProgress: false,
featureLimited: false,
showHTTPS: !window.ddExtension,
};
$scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
$scope.formValues = {
customLogo: false,
KubeconfigExpiry: undefined,
HelmRepositoryURL: undefined,
BlackListedLabels: [],
labelName: '',
labelValue: '',
enableTelemetry: false,
passwordProtect: false,
password: '',
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.$evalAsync(() => {
$scope.formValues.scheduleAutomaticBackups = checked;
});
};
$scope.onBackupOptionsChange = function (type, limited) {
$scope.formValues.backupFormType = type;
$scope.state.featureLimited = limited;
};
$scope.onChangeCheckInInterval = function (interval) {
$scope.$evalAsync(() => {
var settings = $scope.settings;
settings.EdgeAgentCheckinInterval = interval;
});
};
$scope.removeFilteredContainerLabel = function (index) {
const filteredSettings = $scope.formValues.BlackListedLabels.filter((_, i) => i !== index);
const filteredSettingsPayload = { BlackListedLabels: filteredSettings };
updateSettings(filteredSettingsPayload, 'Hidden container settings updated');
};
$scope.addFilteredContainerLabel = function () {
var label = {
name: $scope.formValues.labelName,
value: $scope.formValues.labelValue,
};
const filteredSettings = [...$scope.formValues.BlackListedLabels, label];
const filteredSettingsPayload = { BlackListedLabels: filteredSettings };
updateSettings(filteredSettingsPayload, 'Hidden container settings updated');
};
$scope.downloadBackup = function () {
const payload = {};
if ($scope.formValues.passwordProtect) {
payload.password = $scope.formValues.password;
}
$scope.state.backupInProgress = true;
BackupService.downloadBackup(payload)
.then(function success(data) {
const downloadData = new Blob([data.file], { type: 'application/gzip' });
FileSaver.saveAs(downloadData, data.name);
Notifications.success('Success', 'Backup successfully downloaded');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to download backup');
})
.finally(function final() {
$scope.state.backupInProgress = false;
});
};
// 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
$scope.saveKubernetesSettings = function () {
const kubeSettingsPayload = {
KubeconfigExpiry: $scope.formValues.KubeconfigExpiry,
HelmRepositoryURL: $scope.formValues.HelmRepositoryURL,
GlobalDeploymentOptions: $scope.formValues.GlobalDeploymentOptions,
};
$scope.state.kubeSettingsActionInProgress = true;
updateSettings(kubeSettingsPayload, 'Kubernetes settings updated');
};
function ($scope, Notifications, SettingsService, StateManager) {
$scope.updateSettings = updateSettings;
function updateSettings(settings, successMessage = 'Settings updated') {
SettingsService.update(settings)
.then(function success(response) {
// ignore CloudApiKeys to avoid overriding them
//
// it is not ideal solution as API still accepts CloudAPIKeys
// which may override the cloud provider API keys
settings.CloudApiKeys = undefined;
return SettingsService.update(settings)
.then(function success() {
Notifications.success('Success', successMessage);
StateManager.updateLogo(settings.LogoURL);
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
$scope.initialFormValues.enableTelemetry = response.EnableTelemetry;
$scope.formValues.BlackListedLabels = response.BlackListedLabels;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update settings');
})
.finally(function final() {
$scope.state.kubeSettingsActionInProgress = false;
$scope.state.actionInProgress = false;
});
}
function initView() {
const state = StateManager.getState();
$scope.state.isDemo = state.application.demoEnvironment.enabled;
SettingsService.settings()
.then(function success(data) {
var settings = data;
.then(function success(settings) {
$scope.settings = settings;
if (settings.LogoURL !== '') {
$scope.formValues.customLogo = true;
}
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
$scope.formValues.KubeconfigExpiry = settings.KubeconfigExpiry;
$scope.formValues.HelmRepositoryURL = settings.HelmRepositoryURL;
$scope.formValues.BlackListedLabels = settings.BlackListedLabels;
$scope.initialFormValues.enableTelemetry = settings.EnableTelemetry;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');