mirror of https://github.com/portainer/portainer
feat(app): introduce component library in react [EE-1816] (#6236)
* refactor(app): replace notification with es6 service (#6015) [EE-1897] chore(app): format * refactor(containers): remove the dependency on angular modal service (#6017) [EE-1898] * refactor(app): remove angular from http-request [EE-1899] (#6016) * feat(app): add axios [EE-2035](#6077) * refactor(feature): remove angular dependency from feature service [EE-2034] (#6078) * refactor(app): replace box-selector with react component (#6046) fix: rename angular2react refactor(app): make box-selector type generic feat(app): add story for box-selector feat(app): test box-selector feat(app): add stories for box selector item fix(app): remove unneccesary element refactor(app): remove assign * feat(feature): add be-indicator in react [EE-2005] (#6106) * refactor(app): add react components for headers [EE-1949] (#6023) * feat(auth): provide user context * feat(app): added base header component [EE-1949] style(app): reformat refactor(app/header): use same api as angular * feat(app): add breadcrumbs component [EE-2024] * feat(app): remove u element from user links * fix(users): handle axios errors Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com> * refactor(app): convert switch component to react [EE-2005] (#6025) Co-authored-by: Marcelo Rydel <marcelorydel26@gmail.com>pull/6279/head
parent
eb9f6c77f4
commit
7ae5a3042c
|
@ -13,10 +13,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"*.{j,t}sx"
|
"*.{j,t}sx",
|
||||||
|
"*.ts"
|
||||||
],
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"printWidth": 80,
|
"printWidth": 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import '../app/assets/css';
|
import '../app/assets/css';
|
||||||
|
|
||||||
|
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
controls: {
|
controls: {
|
||||||
|
@ -9,3 +11,11 @@ export const parameters = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||||
|
<Story />
|
||||||
|
</UIRouter>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
|
@ -596,10 +596,6 @@ a[ng-click] {
|
||||||
background-color: var(--bg-log-line-selected-color);
|
background-color: var(--bg-log-line-selected-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row.header .meta .page {
|
|
||||||
padding-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag:not(.token) {
|
.tag:not(.token) {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -739,19 +735,6 @@ a[ng-click] {
|
||||||
}
|
}
|
||||||
/*!toaster override*/
|
/*!toaster override*/
|
||||||
|
|
||||||
/*angular-loading-bar override*/
|
|
||||||
#loadingbar-placeholder {
|
|
||||||
margin-bottom: 0;
|
|
||||||
height: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-bar .bar {
|
|
||||||
position: relative;
|
|
||||||
height: 3px;
|
|
||||||
background: var(--blue-3);
|
|
||||||
}
|
|
||||||
/*!angular-loading-bar override*/
|
|
||||||
|
|
||||||
.monospaced {
|
.monospaced {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -48,94 +48,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Header
|
|
||||||
*/
|
|
||||||
.row.header {
|
|
||||||
height: 60px;
|
|
||||||
background: var(--bg-row-header-color);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
.row.header > div:last-child {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
.row.header .meta .page {
|
|
||||||
font-size: 17px;
|
|
||||||
padding-top: 11px;
|
|
||||||
}
|
|
||||||
.row.header .meta .breadcrumb-links {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.row.header .meta div {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.row.header .login a {
|
|
||||||
padding: 18px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.row.header .user {
|
|
||||||
min-width: 130px;
|
|
||||||
}
|
|
||||||
.row.header .user > .item {
|
|
||||||
width: 65px;
|
|
||||||
height: 60px;
|
|
||||||
float: right;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.row.header .user > .item a {
|
|
||||||
color: #919191;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.row.header .user > .item i {
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 55px;
|
|
||||||
}
|
|
||||||
.row.header .user > .item img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
margin-top: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu {
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.05);
|
|
||||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu .dropdown-header {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu li.link {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu li.link a {
|
|
||||||
padding-left: 7px;
|
|
||||||
padding-right: 7px;
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu:before {
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
right: 23px;
|
|
||||||
display: inline-block;
|
|
||||||
border-right: 7px solid transparent;
|
|
||||||
border-bottom: 7px solid rgba(0, 0, 0, 0.2);
|
|
||||||
border-left: 7px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
.row.header .user > .item ul.dropdown-menu:after {
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
right: 24px;
|
|
||||||
display: inline-block;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid #ffffff;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import toastr from 'toastr';
|
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||||
|
import { agentInterceptor } from './portainer/services/axios';
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
export function configApp($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
|
export function configApp($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
|
||||||
|
@ -21,28 +21,9 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
|
||||||
$httpProvider.defaults.headers.put['Content-Type'] = 'application/json';
|
$httpProvider.defaults.headers.put['Content-Type'] = 'application/json';
|
||||||
$httpProvider.defaults.headers.patch['Content-Type'] = 'application/json';
|
$httpProvider.defaults.headers.patch['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
$httpProvider.interceptors.push(
|
$httpProvider.interceptors.push(() => ({
|
||||||
/* @ngInject */ function (HttpRequestHelper) {
|
request: agentInterceptor,
|
||||||
return {
|
}));
|
||||||
request(config) {
|
|
||||||
if (config.url.indexOf('/docker/') > -1) {
|
|
||||||
config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader();
|
|
||||||
if (HttpRequestHelper.portainerAgentManagerOperation()) {
|
|
||||||
config.headers['X-PortainerAgent-ManagerOperation'] = '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
toastr.options = {
|
|
||||||
timeOut: 3000,
|
|
||||||
closeButton: true,
|
|
||||||
progressBar: true,
|
|
||||||
tapToDismiss: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
Terminal.applyAddon(fit);
|
Terminal.applyAddon(fit);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { HIDE_AUTO_UPDATE_WINDOW } from 'Portainer/feature-flags/feature-ids';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
export default class DockerFeaturesConfigurationController {
|
export default class DockerFeaturesConfigurationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, EndpointService, Notifications, StateManager) {
|
constructor($async, $scope, EndpointService, Notifications, StateManager) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
|
this.$scope = $scope;
|
||||||
this.EndpointService = EndpointService;
|
this.EndpointService = EndpointService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.StateManager = StateManager;
|
this.StateManager = StateManager;
|
||||||
|
|
||||||
this.limitedFeature = HIDE_AUTO_UPDATE_WINDOW;
|
this.limitedFeature = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
enableHostManagementFeatures: false,
|
enableHostManagementFeatures: false,
|
||||||
|
@ -26,9 +27,45 @@ export default class DockerFeaturesConfigurationController {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
autoUpdateSettings: { Enabled: false },
|
||||||
|
timeZone: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.save = this.save.bind(this);
|
this.save = this.save.bind(this);
|
||||||
|
this.onChangeField = this.onChangeField.bind(this);
|
||||||
|
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||||
|
this.onChangeEnableHostManagementFeatures = this.onChangeField('enableHostManagementFeatures');
|
||||||
|
this.onChangeAllowVolumeBrowserForRegularUsers = this.onChangeField('allowVolumeBrowserForRegularUsers');
|
||||||
|
this.onChangeDisableBindMountsForRegularUsers = this.onChangeField('disableBindMountsForRegularUsers');
|
||||||
|
this.onChangeDisablePrivilegedModeForRegularUsers = this.onChangeField('disablePrivilegedModeForRegularUsers');
|
||||||
|
this.onChangeDisableHostNamespaceForRegularUsers = this.onChangeField('disableHostNamespaceForRegularUsers');
|
||||||
|
this.onChangeDisableStackManagementForRegularUsers = this.onChangeField('disableStackManagementForRegularUsers');
|
||||||
|
this.onChangeDisableDeviceMappingForRegularUsers = this.onChangeField('disableDeviceMappingForRegularUsers');
|
||||||
|
this.onChangeDisableContainerCapabilitiesForRegularUsers = this.onChangeField('disableContainerCapabilitiesForRegularUsers');
|
||||||
|
this.onChangeDisableSysctlSettingForRegularUsers = this.onChangeField('disableSysctlSettingForRegularUsers');
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAutoUpdate(value) {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.state.autoUpdateSettings.Enabled = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(values) {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues = {
|
||||||
|
...this.formValues,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeField(field) {
|
||||||
|
return (value) => {
|
||||||
|
this.onChange({
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isContainerEditDisabled() {
|
isContainerEditDisabled() {
|
||||||
|
|
|
@ -20,24 +20,26 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.enableHostManagementFeatures"
|
checked="$ctrl.formValues.enableHostManagementFeatures"
|
||||||
name="enableHostManagementFeatures"
|
name="'enableHostManagementFeatures'"
|
||||||
label="Enable host management features"
|
label="'Enable host management features'"
|
||||||
tooltip="Enable host management features: host system browsing and advanced host details."
|
tooltip="'Enable host management features: host system browsing and advanced host details.'"
|
||||||
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
disabled="!$ctrl.isAgent"
|
disabled="!$ctrl.isAgent"
|
||||||
label-class="col-sm-7 col-lg-4"
|
on-change="($ctrl.onChangeEnableHostManagementFeatures)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
|
checked="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
|
||||||
name="allowVolumeBrowserForRegularUsers"
|
name="'allowVolumeBrowserForRegularUsers'"
|
||||||
label="Enable volume management for non-administrators"
|
label="'Enable volume management for non-administrators'"
|
||||||
tooltip="When enabled, regular users will be able to use Portainer volume management features."
|
tooltip="'When enabled, regular users will be able to use Portainer volume management features.'"
|
||||||
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeAllowVolumeBrowserForRegularUsers)"
|
||||||
disabled="!$ctrl.isAgent"
|
disabled="!$ctrl.isAgent"
|
||||||
label-class="col-sm-7 col-lg-4"
|
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,12 +51,13 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.state.autoUpdateSettings.Enabled"
|
checked="$ctrl.state.autoUpdateSettings.Enabled"
|
||||||
name="disableSysctlSettingForRegularUsers"
|
name="'disableSysctlSettingForRegularUsers'"
|
||||||
label="Enable Change Window"
|
label="'Enable Change Window'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
feature="$ctrl.limitedFeature"
|
feature-id="$ctrl.limitedFeature"
|
||||||
tooltip="Specify a timeframe during which automatic updates can occur in this environment."
|
tooltip="'Specify a time-frame during which automatic updates can occur in this environment.'"
|
||||||
|
on-change="($ctrl.onToggleAutoUpdate)"
|
||||||
>
|
>
|
||||||
</por-switch-field>
|
</por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,73 +70,80 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableBindMountsForRegularUsers"
|
checked="$ctrl.formValues.disableBindMountsForRegularUsers"
|
||||||
name="disableBindMountsForRegularUsers"
|
name="'disableBindMountsForRegularUsers'"
|
||||||
label="Disable bind mounts for non-administrators"
|
label="'Disable bind mounts for non-administrators'"
|
||||||
tooltip="When enabled, regular users will not be able to use bind mounts when creating containers."
|
tooltip="'When enabled, regular users will not be able to use bind mounts when creating containers.'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableBindMountsForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
|
checked="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
|
||||||
name="disablePrivilegedModeForRegularUsers"
|
name="'disablePrivilegedModeForRegularUsers'"
|
||||||
label="Disable privileged mode for non-administrators"
|
label="'Disable privileged mode for non-administrators'"
|
||||||
tooltip="When enabled, regular users will not be able to use privileged mode when creating containers."
|
tooltip="'When enabled, regular users will not be able to use privileged mode when creating containers.'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisablePrivilegedModeForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableHostNamespaceForRegularUsers"
|
checked="$ctrl.formValues.disableHostNamespaceForRegularUsers"
|
||||||
name="disableHostNamespaceForRegularUsers"
|
name="'disableHostNamespaceForRegularUsers'"
|
||||||
label="Disable the use of host PID 1 for non-administrators"
|
label="'Disable the use of host PID 1 for non-administrators'"
|
||||||
tooltip="Prevent users from accessing the host filesystem through the host PID namespace."
|
tooltip="'Prevent users from accessing the host filesystem through the host PID namespace.'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableHostNamespaceForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableStackManagementForRegularUsers"
|
checked="$ctrl.formValues.disableStackManagementForRegularUsers"
|
||||||
name="disableStackManagementForRegularUsers"
|
name="'disableStackManagementForRegularUsers'"
|
||||||
label="Disable the use of Stacks for non-administrators"
|
label="'Disable the use of Stacks for non-administrators'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableStackManagementForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableDeviceMappingForRegularUsers"
|
checked="$ctrl.formValues.disableDeviceMappingForRegularUsers"
|
||||||
name="disableDeviceMappingForRegularUsers"
|
name="'disableDeviceMappingForRegularUsers'"
|
||||||
label="Disable device mappings for non-administrators"
|
label="'Disable device mappings for non-administrators'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableDeviceMappingForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
|
checked="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
|
||||||
name="disableContainerCapabilitiesForRegularUsers"
|
name="'disableContainerCapabilitiesForRegularUsers'"
|
||||||
label="Disable container capabilities for non-administrators"
|
label="'Disable container capabilities for non-administrators'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableContainerCapabilitiesForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.formValues.disableSysctlSettingForRegularUsers"
|
checked="$ctrl.formValues.disableSysctlSettingForRegularUsers"
|
||||||
name="disableSysctlSettingForRegularUsers"
|
name="'disableSysctlSettingForRegularUsers'"
|
||||||
label="Disable sysctl settings for non-administrators"
|
label="'Disable sysctl settings for non-administrators'"
|
||||||
label-class="col-sm-7 col-lg-4"
|
label-class="'col-sm-7 col-lg-4'"
|
||||||
|
on-change="($ctrl.onChangeDisableSysctlSettingForRegularUsers)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Deployment type
|
Deployment type
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="deploymentType" ng-model="$ctrl.value" options="$ctrl.deploymentOptions" on-change="($ctrl.onChange)"></box-selector>
|
<box-selector radio-name="'deploymentType'" value="$ctrl.value" options="$ctrl.deploymentOptions" on-change="($ctrl.onChange)"></box-selector>
|
||||||
|
|
|
@ -23,7 +23,8 @@ class DockerComposeFormController {
|
||||||
this.formValues = values;
|
this.formValues = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeMethod() {
|
onChangeMethod(method) {
|
||||||
|
this.state.Method = method;
|
||||||
this.formValues.StackFileContent = '';
|
this.formValues.StackFileContent = '';
|
||||||
this.selectedTemplate = null;
|
this.selectedTemplate = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="method" ng-model="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
<box-selector radio-name="'method'" value="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="$ctrl.state.Method === 'editor'"
|
ng-if="$ctrl.state.Method === 'editor'"
|
||||||
|
|
|
@ -10,6 +10,7 @@ class KubeManifestFormController {
|
||||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.onChangeFile = this.onChangeFile.bind(this);
|
this.onChangeFile = this.onChangeFile.bind(this);
|
||||||
|
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFormValues(values) {
|
onChangeFormValues(values) {
|
||||||
|
@ -24,6 +25,10 @@ class KubeManifestFormController {
|
||||||
onChangeFile(value) {
|
onChangeFile(value) {
|
||||||
this.formValues.StackFile = value;
|
this.formValues.StackFile = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeMethod(method) {
|
||||||
|
this.state.Method = method;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubeManifestFormController;
|
export default KubeManifestFormController;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="method" ng-model="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
<box-selector radio-name="'method'" value="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="$ctrl.state.Method === 'editor'"
|
ng-if="$ctrl.state.Method === 'editor'"
|
||||||
|
|
|
@ -16,6 +16,11 @@ import './portainer/__module';
|
||||||
import { onStartupAngular } from './app';
|
import { onStartupAngular } from './app';
|
||||||
import { configApp } from './config';
|
import { configApp } from './config';
|
||||||
|
|
||||||
|
import { init as initFeatureService } from './portainer/feature-flags/feature-flags.service';
|
||||||
|
import { Edition } from './portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
initFeatureService(Edition.CE);
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer', [
|
.module('portainer', [
|
||||||
'ui.bootstrap',
|
'ui.bootstrap',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
|
|
||||||
class KubeCreateCustomTemplateViewController {
|
class KubeCreateCustomTemplateViewController {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="method" ng-model="$ctrl.state.method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
<box-selector radio-name="'method'" value="$ctrl.state.method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="$ctrl.state.method === 'editor'"
|
ng-if="$ctrl.state.method === 'editor'"
|
||||||
|
|
|
@ -149,11 +149,12 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.state.autoUpdateSettings.Enabled"
|
checked="ctrl.state.autoUpdateSettings.Enabled"
|
||||||
name="disableSysctlSettingForRegularUsers"
|
name="'disableSysctlSettingForRegularUsers'"
|
||||||
label="Enable Change Window"
|
label="'Enable Change Window'"
|
||||||
feature="ctrl.limitedFeatureAutoWindow"
|
feature-id="ctrl.limitedFeatureAutoWindow"
|
||||||
tooltip="Specify a timeframe during which automatic updates can occur in this environment."
|
tooltip="'Specify a timeframe during which automatic updates can occur in this environment.'"
|
||||||
|
on-change="(ctrl.onToggleAutoUpdate)"
|
||||||
>
|
>
|
||||||
</por-switch-field>
|
</por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -197,12 +198,12 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
label="Allow resource over-commit"
|
data-cy="'kubeSetup-resourceOverCommitToggle'"
|
||||||
name="resource-over-commit-switch"
|
label="'Allow resource over-commit'"
|
||||||
feature="ctrl.limitedFeature"
|
name="'resource-over-commit-switch'"
|
||||||
ng-model="ctrl.formValues.EnableResourceOverCommit"
|
feature-id="ctrl.limitedFeature"
|
||||||
ng-change="ctrl.onChangeEnableResourceOverCommit()"
|
checked="ctrl.formValues.EnableResourceOverCommit"
|
||||||
ng-data-cy="kubeSetup-resourceOverCommitToggle"
|
on-change="(ctrl.onChangeEnableResourceOverCommit)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { K8S_SETUP_DEFAULT } from '@/portainer/feature-flags/feature-ids';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
import { HIDE_AUTO_UPDATE_WINDOW } from 'Portainer/feature-flags/feature-ids';
|
|
||||||
class KubernetesConfigureController {
|
class KubernetesConfigureController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ class KubernetesConfigureController {
|
||||||
constructor(
|
constructor(
|
||||||
$async,
|
$async,
|
||||||
$state,
|
$state,
|
||||||
|
$scope,
|
||||||
Notifications,
|
Notifications,
|
||||||
KubernetesStorageService,
|
KubernetesStorageService,
|
||||||
EndpointService,
|
EndpointService,
|
||||||
|
@ -26,6 +27,7 @@ class KubernetesConfigureController {
|
||||||
) {
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
|
this.$scope = $scope;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.KubernetesStorageService = KubernetesStorageService;
|
this.KubernetesStorageService = KubernetesStorageService;
|
||||||
this.EndpointService = EndpointService;
|
this.EndpointService = EndpointService;
|
||||||
|
@ -39,8 +41,10 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.configureAsync = this.configureAsync.bind(this);
|
this.configureAsync = this.configureAsync.bind(this);
|
||||||
this.limitedFeature = K8S_SETUP_DEFAULT;
|
this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT;
|
||||||
this.limitedFeatureAutoWindow = HIDE_AUTO_UPDATE_WINDOW;
|
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||||
|
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||||
|
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -103,6 +107,15 @@ class KubernetesConfigureController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
onChangeEnableResourceOverCommit(enabled) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.EnableResourceOverCommit = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
this.formValues.ResourceOverCommitPercentage = 20;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* #region CONFIGURE */
|
/* #region CONFIGURE */
|
||||||
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
||||||
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
||||||
|
@ -244,6 +257,12 @@ class KubernetesConfigureController {
|
||||||
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
|
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleAutoUpdate(value) {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.state.autoUpdateSettings.Enabled = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* #region ON INIT */
|
/* #region ON INIT */
|
||||||
async onInit() {
|
async onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
|
@ -39,18 +39,24 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions" data-cy="k8sAppDeploy-buildSelector"></box-selector>
|
<box-selector
|
||||||
|
radio-name="'method'"
|
||||||
|
value="ctrl.state.BuildMethod"
|
||||||
|
options="ctrl.methodOptions"
|
||||||
|
data-cy="k8sAppDeploy-buildSelector"
|
||||||
|
on-change="(ctrl.onChangeMethod)"
|
||||||
|
></box-selector>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.BuildMethod !== ctrl.BuildMethods.CUSTOM_TEMPLATE">
|
<div ng-if="ctrl.state.BuildMethod !== ctrl.BuildMethods.CUSTOM_TEMPLATE">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Deployment type
|
Deployment type
|
||||||
</div>
|
</div>
|
||||||
<box-selector
|
<box-selector
|
||||||
radio-name="deploy"
|
radio-name="'deploy'"
|
||||||
ng-model="ctrl.state.DeployType"
|
value="ctrl.state.DeployType"
|
||||||
on-change="(ctrl.onDeployTypeChange)"
|
|
||||||
options="ctrl.deployOptions"
|
options="ctrl.deployOptions"
|
||||||
data-cy="k8sAppDeploy-deploymentSelector"
|
data-cy="k8sAppDeploy-deploymentSelector"
|
||||||
|
on-change="(ctrl.onChangeDeployType)"
|
||||||
></box-selector>
|
></box-selector>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ import uuidv4 from 'uuid/v4';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||||
|
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, $window, Authentication, ModalService, Notifications, KubernetesResourcePoolService, StackService, WebhookHelper, CustomTemplateService) {
|
constructor($async, $state, $window, Authentication, ModalService, Notifications, KubernetesResourcePoolService, StackService, WebhookHelper, CustomTemplateService) {
|
||||||
|
@ -67,7 +68,8 @@ class KubernetesDeployController {
|
||||||
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
|
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
|
||||||
this.onDeployTypeChange = this.onDeployTypeChange.bind(this);
|
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||||
|
this.onChangeDeployType = this.onChangeDeployType.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAnalyticsProperties() {
|
buildAnalyticsProperties() {
|
||||||
|
@ -122,6 +124,19 @@ class KubernetesDeployController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeMethod(method) {
|
||||||
|
this.state.BuildMethod = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeDeployType(type) {
|
||||||
|
this.state.DeployType = type;
|
||||||
|
if (type == this.ManifestDeployTypes.COMPOSE) {
|
||||||
|
this.DeployMethod = 'compose';
|
||||||
|
} else {
|
||||||
|
this.DeployMethod = 'manifest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disableDeploy() {
|
disableDeploy() {
|
||||||
const isGitFormInvalid =
|
const isGitFormInvalid =
|
||||||
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
||||||
|
@ -275,14 +290,6 @@ class KubernetesDeployController {
|
||||||
return this.$async(this.getNamespacesAsync);
|
return this.$async(this.getNamespacesAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeployTypeChange(value) {
|
|
||||||
if (value == this.ManifestDeployTypes.COMPOSE) {
|
|
||||||
this.DeployMethod = 'compose';
|
|
||||||
} else {
|
|
||||||
this.DeployMethod = 'manifest';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uiCanExit() {
|
async uiCanExit() {
|
||||||
if (this.formValues.EditorContent && this.state.isEditorDirty) {
|
if (this.formValues.EditorContent && this.state.isEditorDirty) {
|
||||||
return this.ModalService.confirmWebEditorDiscard();
|
return this.ModalService.confirmWebEditorDiscard();
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
import controller from './storage-class-switch.controller.js';
|
||||||
|
|
||||||
|
export const storageClassSwitch = {
|
||||||
|
templateUrl: './storage-class-switch.html',
|
||||||
|
controller,
|
||||||
|
bindings: {
|
||||||
|
value: '<',
|
||||||
|
onChange: '<',
|
||||||
|
name: '<',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('portainer.kubernetes').component('storageClassSwitch', storageClassSwitch);
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
class StorageClassSwitchController {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor() {
|
||||||
|
this.featureId = FeatureId.K8S_RESOURCE_POOL_STORAGE_QUOTA;
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(value) {
|
||||||
|
this.onChange(this.name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StorageClassSwitchController;
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<por-switch-field
|
||||||
|
data-cy="'k8sNamespaceCreate-enableQuotaToggle'"
|
||||||
|
label="'Enable quota'"
|
||||||
|
name="'k8s-resourcepool-storagequota'"
|
||||||
|
feature-id="$ctrl.featureId"
|
||||||
|
checked="$ctrl.value"
|
||||||
|
on-change="($ctrl.handleChange)"
|
||||||
|
></por-switch-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -163,11 +163,12 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
|
data-cy="'k8sNamespaceCreate-loadBalancerQuotaToggle'"
|
||||||
label="Load Balancer quota"
|
label="'Load Balancer quota'"
|
||||||
name="k8s-resourcepool-Ibquota"
|
name="'k8s-resourcepool-lbquota'"
|
||||||
feature="$ctrl.LBQuotaFeatureId"
|
feature-id="$ctrl.LBQuotaFeatureId"
|
||||||
ng-model="lbquota"
|
checked="$ctrl.formValues.UseLoadBalancersQuota"
|
||||||
|
on-change="($ctrl.onToggleLoadBalancerQuota)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -189,17 +190,9 @@
|
||||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
standard
|
standard
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
||||||
<por-switch-field
|
|
||||||
ng-data-cy="k8sNamespaceCreate-enableQuotaToggle"
|
|
||||||
label="Enable quota"
|
|
||||||
name="k8s-resourcepool-storagequota"
|
|
||||||
feature="$ctrl.StorageQuotaFeatureId"
|
|
||||||
ng-model="storagequota"
|
|
||||||
></por-switch-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div ng-if="$ctrl.state.canUseIngress">
|
<div ng-if="$ctrl.state.canUseIngress">
|
||||||
|
|
|
@ -12,15 +12,16 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
|
|
||||||
import { K8S_RESOURCE_POOL_LB_QUOTA, K8S_RESOURCE_POOL_STORAGE_QUOTA } from '@/portainer/feature-flags/feature-ids';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
class KubernetesCreateResourcePoolController {
|
class KubernetesCreateResourcePoolController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointService) {
|
constructor($async, $state, $scope, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointService) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
$async,
|
$async,
|
||||||
$state,
|
$state,
|
||||||
|
$scope,
|
||||||
Notifications,
|
Notifications,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
KubernetesResourcePoolService,
|
KubernetesResourcePoolService,
|
||||||
|
@ -30,11 +31,25 @@ class KubernetesCreateResourcePoolController {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||||
this.LBQuotaFeatureId = K8S_RESOURCE_POOL_LB_QUOTA;
|
this.LBQuotaFeatureId = FeatureId.K8S_RESOURCE_POOL_LB_QUOTA;
|
||||||
this.StorageQuotaFeatureId = K8S_RESOURCE_POOL_STORAGE_QUOTA;
|
|
||||||
|
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||||
|
this.onToggleLoadBalancerQuota = this.onToggleLoadBalancerQuota.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
onToggleStorageQuota(storageClassName, enabled) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.StorageClasses = this.formValues.StorageClasses.map((sClass) => (sClass.Name !== storageClassName ? sClass : { ...sClass, Selected: enabled }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleLoadBalancerQuota(enabled) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.UseLoadBalancersQuota = enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onChangeIngressHostname() {
|
onChangeIngressHostname() {
|
||||||
const state = this.state.duplicates.ingressHosts;
|
const state = this.state.duplicates.ingressHosts;
|
||||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||||
|
|
|
@ -147,11 +147,12 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
|
data-cy="'k8sNamespaceCreate-loadBalancerQuotaToggle'"
|
||||||
label="Load Balancer quota"
|
label="'Load Balancer quota'"
|
||||||
name="k8s-resourcepool-Lbquota"
|
name="'k8s-resourcepool-Lbquota'"
|
||||||
feature="ctrl.LBQuotaFeatureId"
|
feature-id="ctrl.LBQuotaFeatureId"
|
||||||
ng-model="lbquota"
|
checked="ctrl.formValues.UseLoadBalancersQuota"
|
||||||
|
on-change="(ctrl.onToggleLoadBalancersQuota)"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -386,17 +387,9 @@
|
||||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
standard
|
standard
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
||||||
<por-switch-field
|
|
||||||
ng-data-cy="k8sNamespaceCreate-enableQuotaToggle"
|
|
||||||
label="Enable quota"
|
|
||||||
name="k8s-resourcepool-storagequota"
|
|
||||||
feature="ctrl.StorageQuotaFeatureId"
|
|
||||||
ng-model="storagequota"
|
|
||||||
></por-switch-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
|
|
|
@ -16,7 +16,7 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
|
||||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { K8S_RESOURCE_POOL_LB_QUOTA, K8S_RESOURCE_POOL_STORAGE_QUOTA } from '@/portainer/feature-flags/feature-ids';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
class KubernetesResourcePoolController {
|
class KubernetesResourcePoolController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -24,6 +24,7 @@ class KubernetesResourcePoolController {
|
||||||
constructor(
|
constructor(
|
||||||
$async,
|
$async,
|
||||||
$state,
|
$state,
|
||||||
|
$scope,
|
||||||
Authentication,
|
Authentication,
|
||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
|
@ -42,6 +43,7 @@ class KubernetesResourcePoolController {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
$async,
|
$async,
|
||||||
$state,
|
$state,
|
||||||
|
$scope,
|
||||||
Authentication,
|
Authentication,
|
||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
|
@ -61,11 +63,13 @@ class KubernetesResourcePoolController {
|
||||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||||
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
|
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
|
||||||
|
|
||||||
this.LBQuotaFeatureId = K8S_RESOURCE_POOL_LB_QUOTA;
|
this.LBQuotaFeatureId = FeatureId.K8S_RESOURCE_POOL_LB_QUOTA;
|
||||||
this.StorageQuotaFeatureId = K8S_RESOURCE_POOL_STORAGE_QUOTA;
|
this.StorageQuotaFeatureId = FeatureId.K8S_RESOURCE_POOL_STORAGE_QUOTA;
|
||||||
|
|
||||||
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
||||||
this.getEvents = this.getEvents.bind(this);
|
this.getEvents = this.getEvents.bind(this);
|
||||||
|
this.onToggleLoadBalancersQuota = this.onToggleLoadBalancersQuota.bind(this);
|
||||||
|
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -80,6 +84,18 @@ class KubernetesResourcePoolController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
onToggleLoadBalancersQuota(checked) {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.UseLoadBalancersQuota = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleStorageQuota(storageClassName, enabled) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.StorageClasses = this.formValues.StorageClasses.map((sClass) => (sClass.Name !== storageClassName ? sClass : { ...sClass, Selected: enabled }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* #region INGRESS MANAGEMENT */
|
/* #region INGRESS MANAGEMENT */
|
||||||
onChangeIngressHostname() {
|
onChangeIngressHostname() {
|
||||||
const state = this.state.duplicates.ingressHosts;
|
const state = this.state.duplicates.ingressHosts;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import componentsModule from './components';
|
||||||
import settingsModule from './settings';
|
import settingsModule from './settings';
|
||||||
import featureFlagModule from './feature-flags';
|
import featureFlagModule from './feature-flags';
|
||||||
import userActivityModule from './user-activity';
|
import userActivityModule from './user-activity';
|
||||||
|
import servicesModule from './services';
|
||||||
|
|
||||||
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||||
authManager.checkAuthOnRefresh();
|
authManager.checkAuthOnRefresh();
|
||||||
|
@ -22,12 +23,19 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
|
||||||
}
|
}
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer.app', ['portainer.oauth', 'portainer.rbac', componentsModule, settingsModule, featureFlagModule, userActivityModule, 'portainer.shared.datatable'])
|
.module('portainer.app', [
|
||||||
|
'portainer.oauth',
|
||||||
|
'portainer.rbac',
|
||||||
|
componentsModule,
|
||||||
|
settingsModule,
|
||||||
|
featureFlagModule,
|
||||||
|
userActivityModule,
|
||||||
|
'portainer.shared.datatable',
|
||||||
|
servicesModule,
|
||||||
|
])
|
||||||
.config([
|
.config([
|
||||||
'$stateRegistryProvider',
|
'$stateRegistryProvider',
|
||||||
function ($stateRegistryProvider) {
|
function ($stateRegistryProvider) {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var root = {
|
var root = {
|
||||||
name: 'root',
|
name: 'root',
|
||||||
abstract: true,
|
abstract: true,
|
||||||
|
@ -56,18 +64,6 @@ angular
|
||||||
controller: 'SidebarController',
|
controller: 'SidebarController',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
|
||||||
featuresServiceInitialized: /* @ngInject */ function featuresServiceInitialized($async, featureService, Notifications) {
|
|
||||||
return $async(async () => {
|
|
||||||
try {
|
|
||||||
await featureService.init();
|
|
||||||
} catch (e) {
|
|
||||||
Notifications.error('Failed initializing features service', e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var endpointRoot = {
|
var endpointRoot = {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { getFeatureDetails } from './utils';
|
||||||
|
|
||||||
|
export default class BeIndicatorController {
|
||||||
|
limitedToBE?: boolean;
|
||||||
|
|
||||||
|
url?: string;
|
||||||
|
|
||||||
|
feature?: FeatureId;
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor() {
|
||||||
|
this.limitedToBE = false;
|
||||||
|
this.url = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
const { url, limitedToBE } = getFeatureDetails(this.feature);
|
||||||
|
|
||||||
|
this.limitedToBE = limitedToBE;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
import { BEFeatureIndicator, Props } from './BEFeatureIndicator';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: BEFeatureIndicator,
|
||||||
|
title: 'Components/BEFeatureIndicator',
|
||||||
|
argTypes: {
|
||||||
|
featureId: {
|
||||||
|
control: { type: 'select', options: Object.values(FeatureId) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta<Props>;
|
||||||
|
|
||||||
|
// : JSX.IntrinsicAttributes & PropsWithChildren<Props>
|
||||||
|
function Template({ featureId }: Props) {
|
||||||
|
initFeatureService(Edition.CE);
|
||||||
|
|
||||||
|
return <BEFeatureIndicator featureId={featureId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example = Template.bind({});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { getFeatureDetails } from './utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
featureId?: FeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BEFeatureIndicator({
|
||||||
|
featureId,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<Props>) {
|
||||||
|
const { url, limitedToBE } = getFeatureDetails(featureId);
|
||||||
|
|
||||||
|
if (!limitedToBE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="be-indicator"
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<i className="fas fa-briefcase space-right be-indicator-icon" />
|
||||||
|
<span className="be-indicator-label">Business Edition Feature</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import controller from './BEFeatureIndicator.controller';
|
||||||
|
|
||||||
|
import './BEFeatureIndicator.css';
|
||||||
|
|
||||||
|
export const beFeatureIndicatorAngular = {
|
||||||
|
templateUrl: './BEFeatureIndicator.html',
|
||||||
|
controller,
|
||||||
|
bindings: {
|
||||||
|
feature: '<',
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BEFeatureIndicator } from './BEFeatureIndicator';
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
const BE_URL = 'https://www.portainer.io/business-upsell?from=';
|
||||||
|
|
||||||
|
export function getFeatureDetails(featureId?: FeatureId) {
|
||||||
|
if (!featureId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const url = `${BE_URL}${featureId}`;
|
||||||
|
|
||||||
|
const limitedToBE = isLimitedToBE(featureId);
|
||||||
|
|
||||||
|
return { url, limitedToBE };
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
.root {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { BoxSelector } from './BoxSelector';
|
||||||
|
import { BoxSelectorOption } from './types';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'BoxSelector',
|
||||||
|
component: BoxSelector,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export function Example() {
|
||||||
|
const [value, setValue] = useState(3);
|
||||||
|
const options: BoxSelectorOption<number>[] = [
|
||||||
|
{
|
||||||
|
description: 'description 1',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '1',
|
||||||
|
value: 3,
|
||||||
|
label: 'option 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'description 2',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '2',
|
||||||
|
value: 4,
|
||||||
|
label: 'option 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxSelector
|
||||||
|
radioName="name"
|
||||||
|
onChange={(value: number) => {
|
||||||
|
setValue(value);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LimitedFeature() {
|
||||||
|
initFeatureService(Edition.CE);
|
||||||
|
const [value, setValue] = useState(3);
|
||||||
|
const options: BoxSelectorOption<number>[] = [
|
||||||
|
{
|
||||||
|
description: 'description 1',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '1',
|
||||||
|
value: 3,
|
||||||
|
label: 'option 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'description 2',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '2',
|
||||||
|
value: 4,
|
||||||
|
label: 'option 2',
|
||||||
|
feature: FeatureId.ACTIVITY_AUDIT,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxSelector
|
||||||
|
radioName="name"
|
||||||
|
onChange={(value: number) => {
|
||||||
|
setValue(value);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// regular example
|
||||||
|
|
||||||
|
// story with limited feature
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { render, fireEvent } from '@/react-tools/test-utils';
|
||||||
|
|
||||||
|
import { BoxSelector, Props } from './BoxSelector';
|
||||||
|
import { BoxSelectorOption } from './types';
|
||||||
|
|
||||||
|
function renderDefault<T extends string | number>({
|
||||||
|
options = [],
|
||||||
|
onChange = () => {},
|
||||||
|
radioName = 'radio',
|
||||||
|
value,
|
||||||
|
}: Partial<Props<T>> = {}) {
|
||||||
|
return render(
|
||||||
|
<BoxSelector
|
||||||
|
options={options}
|
||||||
|
onChange={onChange}
|
||||||
|
radioName={radioName}
|
||||||
|
value={value || 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should render with the initial value selected and call onChange when clicking a different value', async () => {
|
||||||
|
const options: BoxSelectorOption<number>[] = [
|
||||||
|
{
|
||||||
|
description: 'description 1',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '1',
|
||||||
|
value: 3,
|
||||||
|
label: 'option 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'description 2',
|
||||||
|
icon: 'fa fa-rocket',
|
||||||
|
id: '2',
|
||||||
|
value: 4,
|
||||||
|
label: 'option 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const { getByLabelText } = renderDefault({
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
value: options[0].value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const item1 = getByLabelText(options[0].label, {
|
||||||
|
exact: false,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(item1.checked).toBeTruthy();
|
||||||
|
|
||||||
|
const item2 = getByLabelText(options[1].label, {
|
||||||
|
exact: false,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(item2.checked).toBeFalsy();
|
||||||
|
|
||||||
|
fireEvent.click(item2);
|
||||||
|
expect(onChange).toHaveBeenCalledWith(options[1].value, false);
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import type { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import './BoxSelector.css';
|
||||||
|
import styles from './BoxSelector.module.css';
|
||||||
|
import { BoxSelectorItem } from './BoxSelectorItem';
|
||||||
|
import { BoxSelectorOption } from './types';
|
||||||
|
|
||||||
|
export interface Props<T extends number | string> {
|
||||||
|
radioName: string;
|
||||||
|
value: T;
|
||||||
|
onChange(value: T, limitedToBE: boolean): void;
|
||||||
|
options: BoxSelectorOption<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoxSelector<T extends number | string>({
|
||||||
|
radioName,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
}: Props<T>) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('boxselector_wrapper', styles.root)}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<BoxSelectorItem
|
||||||
|
key={option.id}
|
||||||
|
radioName={radioName}
|
||||||
|
option={option}
|
||||||
|
onChange={onChange}
|
||||||
|
selectedValue={value}
|
||||||
|
disabled={option.disabled && option.disabled()}
|
||||||
|
tooltip={option.tooltip && option.tooltip()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOption<T extends number | string>(
|
||||||
|
id: string,
|
||||||
|
icon: string,
|
||||||
|
label: string,
|
||||||
|
description: string,
|
||||||
|
value: T,
|
||||||
|
feature: FeatureId
|
||||||
|
): BoxSelectorOption<T> {
|
||||||
|
return { id, icon, label, description, value, feature };
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
IComponentOptions,
|
||||||
|
IComponentController,
|
||||||
|
IFormController,
|
||||||
|
IScope,
|
||||||
|
} from 'angular';
|
||||||
|
|
||||||
|
class BoxSelectorController implements IComponentController {
|
||||||
|
formCtrl!: IFormController;
|
||||||
|
|
||||||
|
onChange!: (value: string | number) => void;
|
||||||
|
|
||||||
|
radioName!: string;
|
||||||
|
|
||||||
|
$scope: IScope;
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($scope: IScope) {
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
|
||||||
|
this.$scope = $scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(value: string | number, limitedToBE: boolean) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formCtrl.$setValidity(this.radioName, !limitedToBE, this.formCtrl);
|
||||||
|
this.onChange(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BoxSelectorAngular: IComponentOptions = {
|
||||||
|
template: `<box-selector-react
|
||||||
|
value="$ctrl.value"
|
||||||
|
on-change="$ctrl.handleChange"
|
||||||
|
options="$ctrl.options"
|
||||||
|
radio-name="$ctrl.radioName"
|
||||||
|
></box-selector-react>`,
|
||||||
|
bindings: {
|
||||||
|
value: '<',
|
||||||
|
onChange: '<',
|
||||||
|
options: '<',
|
||||||
|
radioName: '<',
|
||||||
|
},
|
||||||
|
require: {
|
||||||
|
formCtrl: '^form',
|
||||||
|
},
|
||||||
|
controller: BoxSelectorController,
|
||||||
|
};
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
|
||||||
|
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { BoxSelectorItem } from './BoxSelectorItem';
|
||||||
|
import { BoxSelectorOption } from './types';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'BoxSelector/Item',
|
||||||
|
args: {
|
||||||
|
selected: false,
|
||||||
|
description: 'description',
|
||||||
|
icon: 'fa-rocket',
|
||||||
|
label: 'label',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
interface ExampleProps {
|
||||||
|
selected?: boolean;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
label?: string;
|
||||||
|
feature?: FeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Template({
|
||||||
|
selected,
|
||||||
|
description = 'description',
|
||||||
|
icon,
|
||||||
|
label = 'label',
|
||||||
|
feature,
|
||||||
|
}: ExampleProps) {
|
||||||
|
const option: BoxSelectorOption<number> = {
|
||||||
|
description,
|
||||||
|
icon: `fa ${icon}`,
|
||||||
|
id: 'id',
|
||||||
|
label,
|
||||||
|
value: 1,
|
||||||
|
feature,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="boxselector_wrapper">
|
||||||
|
<BoxSelectorItem
|
||||||
|
onChange={() => {}}
|
||||||
|
option={option}
|
||||||
|
radioName="radio"
|
||||||
|
selectedValue={selected ? option.value : 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example = Template.bind({});
|
||||||
|
|
||||||
|
export function SelectedItem() {
|
||||||
|
return <Template selected />;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedItem.args = {
|
||||||
|
selected: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LimitedFeatureItem() {
|
||||||
|
initFeatureService(Edition.CE);
|
||||||
|
|
||||||
|
return <Template feature={FeatureId.ACTIVITY_AUDIT} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectedLimitedFeatureItem() {
|
||||||
|
initFeatureService(Edition.CE);
|
||||||
|
|
||||||
|
return <Template feature={FeatureId.ACTIVITY_AUDIT} selected />;
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
|
||||||
|
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
import './BoxSelectorItem.css';
|
||||||
|
|
||||||
|
import { BoxSelectorOption } from './types';
|
||||||
|
|
||||||
|
interface Props<T extends number | string> {
|
||||||
|
radioName: string;
|
||||||
|
option: BoxSelectorOption<T>;
|
||||||
|
onChange(value: T, limitedToBE: boolean): void;
|
||||||
|
selectedValue: T;
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoxSelectorItem<T extends number | string>({
|
||||||
|
radioName,
|
||||||
|
option,
|
||||||
|
onChange,
|
||||||
|
selectedValue,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
}: Props<T>) {
|
||||||
|
const limitedToBE = isLimitedToBE(option.feature);
|
||||||
|
|
||||||
|
const tooltipId = `box-selector-item-${radioName}-${option.id}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('box-selector-item', {
|
||||||
|
business: limitedToBE,
|
||||||
|
limited: limitedToBE,
|
||||||
|
})}
|
||||||
|
data-tip
|
||||||
|
data-for={tooltipId}
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={radioName}
|
||||||
|
id={option.id}
|
||||||
|
checked={option.value === selectedValue}
|
||||||
|
value={option.value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={() => onChange(option.value, limitedToBE)}
|
||||||
|
/>
|
||||||
|
<label htmlFor={option.id} data-cy={`${radioName}_${option.value}`}>
|
||||||
|
{limitedToBE && <i className="fas fa-briefcase limited-icon" />}
|
||||||
|
|
||||||
|
<div className="boxselector_header">
|
||||||
|
{!!option.icon && (
|
||||||
|
<i
|
||||||
|
className={clsx(option.icon, 'space-right')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="box-selector-item-description">{option.description}</p>
|
||||||
|
</label>
|
||||||
|
{tooltip && (
|
||||||
|
<ReactTooltip
|
||||||
|
place="bottom"
|
||||||
|
className="portainer-tooltip"
|
||||||
|
id={tooltipId}
|
||||||
|
>
|
||||||
|
{tooltip}
|
||||||
|
</ReactTooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { BoxSelector, buildOption } from './BoxSelector';
|
||||||
|
import { BoxSelectorAngular } from './BoxSelectorAngular';
|
||||||
|
|
||||||
|
export { BoxSelector, buildOption };
|
||||||
|
const BoxSelectorReact = react2angular(BoxSelector, ['value', 'onChange', 'options', 'radioName']);
|
||||||
|
|
||||||
|
export default angular.module('app.portainer.component.box-selector', []).component('boxSelectorReact', BoxSelectorReact).component('boxSelector', BoxSelectorAngular).name;
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
export interface BoxSelectorOption<T> {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
value: T;
|
||||||
|
disabled?: () => boolean;
|
||||||
|
tooltip?: () => string;
|
||||||
|
feature?: FeatureId;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.breadcrumb-links {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import { UIRouter, pushStateLocationPlugin } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { Link } from '@/portainer/components/Link';
|
||||||
|
|
||||||
|
import { Breadcrumbs } from './Breadcrumbs';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Header/Breadcrumbs',
|
||||||
|
component: Breadcrumbs,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export function Example() {
|
||||||
|
return (
|
||||||
|
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Link to="portainer.endpoints">Environments</Link>
|
||||||
|
<Link to="portainer.endpoints.endpoint({id: endpoint.Id})">
|
||||||
|
endpointName
|
||||||
|
</Link>
|
||||||
|
String item
|
||||||
|
</Breadcrumbs>
|
||||||
|
</UIRouter>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Breadcrumbs.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Breadcrumbs({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="breadcrumb-links">
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<>
|
||||||
|
{child}
|
||||||
|
{index !== children.length - 1 ? ' > ' : ''}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
.row.header .meta .page {
|
||||||
|
padding-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.hamburg .row.header .meta {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.header {
|
||||||
|
height: 60px;
|
||||||
|
background: var(--bg-row-header-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.row.header > div:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.row.header .meta .page {
|
||||||
|
font-size: 17px;
|
||||||
|
padding-top: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.header .meta div {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.row.header .login a {
|
||||||
|
padding: 18px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.row.header .user {
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
.row.header .user > .item {
|
||||||
|
width: 65px;
|
||||||
|
height: 60px;
|
||||||
|
float: right;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.row.header .user > .item a {
|
||||||
|
color: #919191;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.row.header .user > .item i {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 55px;
|
||||||
|
}
|
||||||
|
.row.header .user > .item img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu {
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu .dropdown-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu li.link {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu li.link a {
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu:before {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
right: 23px;
|
||||||
|
display: inline-block;
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-bottom: 7px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.row.header .user > .item ul.dropdown-menu:after {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid #ffffff;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*angular-loading-bar override*/
|
||||||
|
#loadingbar-placeholder {
|
||||||
|
margin-bottom: 0;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar .bar {
|
||||||
|
position: relative;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--blue-3);
|
||||||
|
}
|
||||||
|
/*!angular-loading-bar override*/
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div class="row header">
|
||||||
|
<div id="loadingbar-placeholder"></div>
|
||||||
|
<div class="col-xs-12"> <div class="meta" ng-transclude></div></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Link } from '@/portainer/components/Link';
|
||||||
|
import { UserContext } from '@/portainer/hooks/useUser';
|
||||||
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
|
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
|
||||||
|
|
||||||
|
import { HeaderContent, HeaderTitle } from '.';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Header,
|
||||||
|
title: 'Components/Header',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
interface StoryProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Template({ title }: StoryProps) {
|
||||||
|
return (
|
||||||
|
<UserContext.Provider
|
||||||
|
value={{ user: new UserViewModel({ Username: 'test' }) }}
|
||||||
|
>
|
||||||
|
<Header>
|
||||||
|
<HeaderTitle title={title} />
|
||||||
|
<HeaderContent>
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Link to="example">Container instances</Link>
|
||||||
|
Add container
|
||||||
|
</Breadcrumbs>
|
||||||
|
</HeaderContent>
|
||||||
|
</Header>
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<StoryProps> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
title: 'Container details',
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import './Header.css';
|
||||||
|
|
||||||
|
const Context = createContext<null | boolean>(null);
|
||||||
|
|
||||||
|
export function useHeaderContext() {
|
||||||
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
if (context == null) {
|
||||||
|
throw new Error('Should be nested inside a Header component');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ children }: PropsWithChildren<unknown>) {
|
||||||
|
return (
|
||||||
|
<Context.Provider value>
|
||||||
|
<div className="row header">
|
||||||
|
<div id="loadingbar-placeholder" />
|
||||||
|
<div className="col-xs-12">
|
||||||
|
<div className="meta">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderAngular = {
|
||||||
|
transclude: true,
|
||||||
|
templateUrl: './Header.html',
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default class HeaderContentController {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(Authentication) {
|
||||||
|
this.Authentication = Authentication;
|
||||||
|
|
||||||
|
this.username = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
const userDetails = this.Authentication.getUserDetails();
|
||||||
|
if (userDetails) {
|
||||||
|
this.username = userDetails.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="breadcrumb-links">
|
||||||
|
<div class="pull-left" ng-transclude></div>
|
||||||
|
<div class="pull-right" ng-if="$ctrl.username">
|
||||||
|
<a ui-sref="portainer.account" style="margin-right: 5px;">
|
||||||
|
<u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
|
||||||
|
</a>
|
||||||
|
<a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;" data-cy="template-logoutButton">
|
||||||
|
<u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,19 @@
|
||||||
|
.user-links {
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-links > * + * {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link .link-text {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link .link-icon {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { Link } from '@/portainer/components/Link';
|
||||||
|
import { useUser } from '@/portainer/hooks/useUser';
|
||||||
|
|
||||||
|
import controller from './HeaderContent.controller';
|
||||||
|
import styles from './HeaderContent.module.css';
|
||||||
|
import { useHeaderContext } from './Header';
|
||||||
|
|
||||||
|
export function HeaderContent({ children }: PropsWithChildren<unknown>) {
|
||||||
|
useHeaderContext();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="breadcrumb-links">
|
||||||
|
<div className="pull-left">{children}</div>
|
||||||
|
{user && (
|
||||||
|
<div className={clsx('pull-right', styles.userLinks)}>
|
||||||
|
<Link to="portainer.account" className={styles.link}>
|
||||||
|
<i
|
||||||
|
className={clsx('fa fa-wrench', styles.linkIcon)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className={styles.linkText}>my account</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="portainer.logout"
|
||||||
|
params={{ performApiLogout: true }}
|
||||||
|
className={clsx('text-danger', styles.link)}
|
||||||
|
data-cy="template-logoutButton"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={clsx('fa fa-sign-out-alt', styles.linkIcon)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className={styles.linkText}>log out</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderContentAngular = {
|
||||||
|
requires: '^rdHeader',
|
||||||
|
transclude: true,
|
||||||
|
templateUrl: './HeaderContent.html',
|
||||||
|
controller,
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default class HeaderTitle {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor(Authentication) {
|
||||||
|
this.Authentication = Authentication;
|
||||||
|
|
||||||
|
this.username = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
const userDetails = this.Authentication.getUserDetails();
|
||||||
|
if (userDetails) {
|
||||||
|
this.username = userDetails.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="page white-space-normal">
|
||||||
|
{{ $ctrl.titleText }}
|
||||||
|
<span class="header_title_content" ng-transclude></span>
|
||||||
|
<span class="pull-right user-box" ng-if="$ctrl.username"> <i class="fa fa-user-circle" aria-hidden="true"></i> {{ $ctrl.username }} </span>
|
||||||
|
</div>
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { useUser } from '@/portainer/hooks/useUser';
|
||||||
|
|
||||||
|
import { useHeaderContext } from './Header';
|
||||||
|
import controller from './HeaderTitle.controller';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
|
||||||
|
useHeaderContext();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page white-space-normal">
|
||||||
|
{title}
|
||||||
|
<span className="header_title_content">{children}</span>
|
||||||
|
{user && (
|
||||||
|
<span className="pull-right user-box">
|
||||||
|
<i className="fa fa-user-circle" aria-hidden="true" /> {user.Username}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderTitleAngular = {
|
||||||
|
requires: '^rdHeader',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@',
|
||||||
|
},
|
||||||
|
transclude: true,
|
||||||
|
templateUrl: './HeaderTitle.html',
|
||||||
|
controller,
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { Header, HeaderAngular } from './Header';
|
||||||
|
import { HeaderContent, HeaderContentAngular } from './HeaderContent';
|
||||||
|
import { HeaderTitle, HeaderTitleAngular } from './HeaderTitle';
|
||||||
|
|
||||||
|
export { Header, HeaderTitle, HeaderContent };
|
||||||
|
|
||||||
|
export default angular
|
||||||
|
.module('portainer.app.components.header', [])
|
||||||
|
|
||||||
|
.component('rdHeader', HeaderAngular)
|
||||||
|
.component('rdHeaderContent', HeaderContentAngular)
|
||||||
|
.component('rdHeaderTitle', HeaderTitleAngular).name;
|
|
@ -1,15 +1,20 @@
|
||||||
import { ReactNode } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { UISref, UISrefProps } from '@uirouter/react';
|
import { UISref, UISrefProps } from '@uirouter/react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function Link({
|
export function Link({
|
||||||
|
title = '',
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: { children: ReactNode } & UISrefProps) {
|
}: PropsWithChildren<Props> & UISrefProps) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<UISref {...props}>
|
<UISref {...props}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a>{children}</a>
|
<a title={title}>{children}</a>
|
||||||
</UISref>
|
</UISref>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,12 @@ import _ from 'lodash-es';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { RoleTypes } from '@/portainer/rbac/models/role';
|
import { RoleTypes } from '@/portainer/rbac/models/role';
|
||||||
|
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class PorAccessManagementController {
|
class PorAccessManagementController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(Notifications, AccessService, RoleService, featureService) {
|
constructor(Notifications, AccessService, RoleService) {
|
||||||
Object.assign(this, { Notifications, AccessService, RoleService, featureService });
|
Object.assign(this, { Notifications, AccessService, RoleService });
|
||||||
|
|
||||||
this.limitedToBE = false;
|
this.limitedToBE = false;
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class PorAccessManagementController {
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
try {
|
try {
|
||||||
if (this.limitedFeature) {
|
if (this.limitedFeature) {
|
||||||
this.limitedToBE = this.featureService.isLimitedToBE(this.limitedFeature);
|
this.limitedToBE = isLimitedToBE(this.limitedFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity = this.accessControlledEntity;
|
const entity = this.accessControlledEntity;
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
const BE_URL = 'https://www.portainer.io/business-upsell?from=';
|
|
||||||
|
|
||||||
export default class BeIndicatorController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(featureService) {
|
|
||||||
Object.assign(this, { featureService });
|
|
||||||
|
|
||||||
this.limitedToBE = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
if (this.feature) {
|
|
||||||
this.url = `${BE_URL}${this.feature}`;
|
|
||||||
|
|
||||||
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import controller from './be-feature-indicator.controller.js';
|
|
||||||
|
|
||||||
import './be-feature-indicator.css';
|
|
||||||
|
|
||||||
export const beFeatureIndicator = {
|
|
||||||
templateUrl: './be-feature-indicator.html',
|
|
||||||
controller,
|
|
||||||
bindings: {
|
|
||||||
feature: '<',
|
|
||||||
},
|
|
||||||
transclude: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('beFeatureIndicator', beFeatureIndicator);
|
|
|
@ -1,23 +0,0 @@
|
||||||
export default class BoxSelectorItemController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(featureService) {
|
|
||||||
Object.assign(this, { featureService });
|
|
||||||
|
|
||||||
this.limitedToBE = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange(value) {
|
|
||||||
this.formCtrl.$setValidity(this.radioName, !this.limitedToBE, this.formCtrl);
|
|
||||||
this.onChange(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
if (this.option.feature) {
|
|
||||||
this.limitedToBE = this.featureService.isLimitedToBE(this.option.feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$onDestroy() {
|
|
||||||
this.formCtrl.$setValidity(this.radioName, true, this.formCtrl);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
<div
|
|
||||||
class="box-selector-item"
|
|
||||||
ng-class="{ business: $ctrl.limitedToBE, limited: $ctrl.limitedToBE }"
|
|
||||||
tooltip-append-to-body="true"
|
|
||||||
tooltip-placement="bottom"
|
|
||||||
tooltip-class="portainer-tooltip"
|
|
||||||
tooltip-enable="$ctrl.tooltip"
|
|
||||||
uib-tooltip="{{ $ctrl.tooltip }}"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="{{ $ctrl.radioName }}"
|
|
||||||
id="{{ $ctrl.option.id }}"
|
|
||||||
ng-checked="$ctrl.isChecked($ctrl.option.value)"
|
|
||||||
ng-value="$ctrl.option.value"
|
|
||||||
ng-disabled="$ctrl.disabled"
|
|
||||||
/>
|
|
||||||
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.handleChange($ctrl.option.value)" data-cy="{{ $ctrl.radioName }}_{{ $ctrl.option.value }}">
|
|
||||||
<i class="fas fa-briefcase limited-icon" ng-if="$ctrl.limitedToBE"></i>
|
|
||||||
|
|
||||||
<div class="boxselector_header">
|
|
||||||
<i ng-class="$ctrl.option.icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
{{ $ctrl.option.label }}
|
|
||||||
</div>
|
|
||||||
<p class="box-selector-item-description">{{ $ctrl.option.description }}</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
|
@ -1,21 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import './box-selector-item.css';
|
|
||||||
|
|
||||||
import controller from './box-selector-item.controller';
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('boxSelectorItem', {
|
|
||||||
templateUrl: './box-selector-item.html',
|
|
||||||
controller,
|
|
||||||
require: {
|
|
||||||
formCtrl: '^^form',
|
|
||||||
},
|
|
||||||
bindings: {
|
|
||||||
radioName: '@',
|
|
||||||
isChecked: '<',
|
|
||||||
option: '<',
|
|
||||||
onChange: '<',
|
|
||||||
disabled: '<',
|
|
||||||
tooltip: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
export default class BoxSelectorController {
|
|
||||||
constructor() {
|
|
||||||
this.isChecked = this.isChecked.bind(this);
|
|
||||||
this.change = this.change.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
change(value, limited) {
|
|
||||||
this.ngModel = value;
|
|
||||||
if (this.onChange) {
|
|
||||||
this.onChange(value, limited);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isChecked(value) {
|
|
||||||
return this.ngModel === value;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
<div class="form-group"></div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="boxselector_wrapper">
|
|
||||||
<box-selector-item
|
|
||||||
ng-repeat="option in $ctrl.options"
|
|
||||||
radio-name="{{ $ctrl.radioName }}"
|
|
||||||
option="option"
|
|
||||||
on-change="($ctrl.change)"
|
|
||||||
is-checked="$ctrl.isChecked"
|
|
||||||
disabled="option.disabled()"
|
|
||||||
tooltip="option.tooltip()"
|
|
||||||
></box-selector-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,20 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import './box-selector.css';
|
|
||||||
|
|
||||||
import controller from './box-selector.controller';
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('boxSelector', {
|
|
||||||
templateUrl: './box-selector.html',
|
|
||||||
controller,
|
|
||||||
bindings: {
|
|
||||||
radioName: '@',
|
|
||||||
ngModel: '=',
|
|
||||||
options: '<',
|
|
||||||
onChange: '<',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function buildOption(id, icon, label, description, value, feature) {
|
|
||||||
return { id, icon, label, description, value, feature };
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||||
import { REGISTRY_MANAGEMENT } from '@/portainer/feature-flags/feature-ids';
|
|
||||||
angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController);
|
angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController);
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -45,7 +46,7 @@ function RegistriesDatatableController($scope, $controller, $state, Authenticati
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function () {
|
this.$onInit = function () {
|
||||||
this.limitedFeature = REGISTRY_MANAGEMENT;
|
this.limitedFeature = FeatureId.REGISTRY_MANAGEMENT;
|
||||||
this.isAdmin = Authentication.isAdmin();
|
this.isAdmin = Authentication.isAdmin();
|
||||||
this.setDefaults();
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.root {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Switch } from './Switch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Form/SwitchField/Switch',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export function Example() {
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
function onChange() {
|
||||||
|
setIsChecked(!isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Switch name="name" checked={isChecked} onChange={onChange} id="id" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Template({ checked }: Args) {
|
||||||
|
return <Switch name="name" checked={checked} onChange={() => {}} id="id" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checked: Story<Args> = Template.bind({});
|
||||||
|
Checked.args = {
|
||||||
|
checked: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Unchecked: Story<Args> = Template.bind({});
|
||||||
|
Unchecked.args = {
|
||||||
|
checked: false,
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { Switch, Props } from './Switch';
|
||||||
|
|
||||||
|
function renderDefault({
|
||||||
|
name = 'default name',
|
||||||
|
checked = false,
|
||||||
|
}: Partial<PropsWithChildren<Props>> = {}) {
|
||||||
|
return render(
|
||||||
|
<Switch id="id" name={name} checked={checked} onChange={() => {}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display a Switch component', async () => {
|
||||||
|
const { findByRole } = renderDefault();
|
||||||
|
|
||||||
|
const switchElem = await findByRole('checkbox');
|
||||||
|
expect(switchElem).toBeTruthy();
|
||||||
|
});
|
|
@ -0,0 +1,68 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
import { BEFeatureIndicator } from '@/portainer/components/BEFeatureIndicator';
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import './Switch.css';
|
||||||
|
|
||||||
|
import styles from './Switch.module.css';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
checked: boolean;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
onChange(checked: boolean): void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
dataCy?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
featureId?: FeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Switch({
|
||||||
|
name,
|
||||||
|
checked,
|
||||||
|
id,
|
||||||
|
disabled,
|
||||||
|
dataCy,
|
||||||
|
onChange,
|
||||||
|
featureId,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const limitedToBE = isLimitedToBE(featureId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
className={clsx('switch', className, styles.root, {
|
||||||
|
business: limitedToBE,
|
||||||
|
limited: limitedToBE,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={name}
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled || limitedToBE}
|
||||||
|
onChange={({ target: { checked } }) => onChange(checked)}
|
||||||
|
/>
|
||||||
|
<i data-cy={dataCy} />
|
||||||
|
</label>
|
||||||
|
{limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchAngular = r2a(Switch, [
|
||||||
|
'name',
|
||||||
|
'checked',
|
||||||
|
'id',
|
||||||
|
'disabled',
|
||||||
|
'dataCy',
|
||||||
|
'onChange',
|
||||||
|
'feature',
|
||||||
|
'className',
|
||||||
|
]);
|
|
@ -0,0 +1,9 @@
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { SwitchField } from './SwitchField';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Form/SwitchField',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export function Example() {
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
function onChange() {
|
||||||
|
setIsChecked(!isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwitchField
|
||||||
|
name="name"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={onChange}
|
||||||
|
label="Example"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
checked: boolean;
|
||||||
|
label: string;
|
||||||
|
labelClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Template({ checked, label, labelClass }: Args) {
|
||||||
|
return (
|
||||||
|
<SwitchField
|
||||||
|
name="name"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => {}}
|
||||||
|
label={label}
|
||||||
|
labelClass={labelClass}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checked: Story<Args> = Template.bind({});
|
||||||
|
Checked.args = {
|
||||||
|
checked: true,
|
||||||
|
label: 'label',
|
||||||
|
labelClass: 'col-sm-6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Unchecked: Story<Args> = Template.bind({});
|
||||||
|
Unchecked.args = {
|
||||||
|
checked: false,
|
||||||
|
label: 'label',
|
||||||
|
labelClass: 'col-sm-6',
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { render, fireEvent } from '@/react-tools/test-utils';
|
||||||
|
|
||||||
|
import { SwitchField, Props } from './SwitchField';
|
||||||
|
|
||||||
|
function renderDefault({
|
||||||
|
name = 'default name',
|
||||||
|
checked = false,
|
||||||
|
label = 'label',
|
||||||
|
onChange = jest.fn(),
|
||||||
|
}: Partial<Props> = {}) {
|
||||||
|
return render(
|
||||||
|
<SwitchField
|
||||||
|
label={label}
|
||||||
|
name={name}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should display a Switch component', async () => {
|
||||||
|
const { findByRole } = renderDefault();
|
||||||
|
|
||||||
|
const switchElem = await findByRole('checkbox');
|
||||||
|
expect(switchElem).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking should emit on-change with the opposite value', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const checked = true;
|
||||||
|
const { findByRole } = renderDefault({ onChange, checked });
|
||||||
|
|
||||||
|
const switchElem = await findByRole('checkbox');
|
||||||
|
fireEvent.click(switchElem);
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(!checked);
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { Tooltip } from '@/portainer/components/Tip/Tooltip';
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import styles from './SwitchField.module.css';
|
||||||
|
import { Switch } from './Switch';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange(value: boolean): void;
|
||||||
|
|
||||||
|
name?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
labelClass?: string;
|
||||||
|
dataCy?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
featureId?: FeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwitchField({
|
||||||
|
tooltip,
|
||||||
|
checked,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
labelClass,
|
||||||
|
dataCy,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
featureId,
|
||||||
|
}: Props) {
|
||||||
|
const toggleName = name ? `toggle_${name}` : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={styles.root}>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'control-label text-left space-right',
|
||||||
|
styles.label,
|
||||||
|
labelClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{tooltip && <Tooltip position="bottom" message={tooltip} />}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
className="space-right"
|
||||||
|
name={toggleName}
|
||||||
|
id={toggleName}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
featureId={featureId}
|
||||||
|
dataCy={dataCy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchFieldAngular = r2a(SwitchField, [
|
||||||
|
'tooltip',
|
||||||
|
'checked',
|
||||||
|
'label',
|
||||||
|
'name',
|
||||||
|
'labelClass',
|
||||||
|
'dataCy',
|
||||||
|
'disabled',
|
||||||
|
'onChange',
|
||||||
|
'featureId',
|
||||||
|
]);
|
|
@ -0,0 +1 @@
|
||||||
|
export { SwitchField, SwitchFieldAngular } from './SwitchField';
|
|
@ -3,4 +3,10 @@ import angular from 'angular';
|
||||||
import { webEditorForm } from './web-editor-form';
|
import { webEditorForm } from './web-editor-form';
|
||||||
import { fileUploadForm } from './file-upload-form';
|
import { fileUploadForm } from './file-upload-form';
|
||||||
|
|
||||||
export default angular.module('portainer.app.components.form', []).component('webEditorForm', webEditorForm).component('fileUploadForm', fileUploadForm).name;
|
import { SwitchFieldAngular } from './SwitchField';
|
||||||
|
|
||||||
|
export default angular
|
||||||
|
.module('portainer.app.components.form', [])
|
||||||
|
.component('webEditorForm', webEditorForm)
|
||||||
|
.component('fileUploadForm', fileUploadForm)
|
||||||
|
.component('porSwitchField', SwitchFieldAngular).name;
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
class GitFormComposeAuthFieldsetController {
|
class GitFormComposeAuthFieldsetController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor() {
|
constructor($scope) {
|
||||||
|
Object.assign(this, { $scope });
|
||||||
|
|
||||||
this.authValues = {
|
this.authValues = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.onChangeField = this.onChangeField.bind(this);
|
this.onChangeField = this.onChangeField.bind(this);
|
||||||
this.onChangeAuth = this.onChangeAuth.bind(this);
|
this.onChangeAuth = this.onChangeAuth.bind(this);
|
||||||
this.onChangeUsername = this.onChangeField('RepositoryUsername');
|
this.onChangeUsername = this.onChangeField('RepositoryUsername');
|
||||||
this.onChangePassword = this.onChangeField('RepositoryPassword');
|
this.onChangePassword = this.onChangeField('RepositoryPassword');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChange(...args) {
|
||||||
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.onChange(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onChangeField(field) {
|
onChangeField(field) {
|
||||||
return (value) => {
|
return (value) => {
|
||||||
this.onChange({
|
this.handleChange({
|
||||||
...this.model,
|
...this.model,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
});
|
});
|
||||||
|
@ -27,7 +36,7 @@ class GitFormComposeAuthFieldsetController {
|
||||||
this.authValues.password = this.model.RepositoryPassword;
|
this.authValues.password = this.model.RepositoryPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onChange({
|
this.handleChange({
|
||||||
...this.model,
|
...this.model,
|
||||||
RepositoryAuthentication: auth,
|
RepositoryAuthentication: auth,
|
||||||
RepositoryUsername: auth ? this.authValues.username : '',
|
RepositoryUsername: auth ? this.authValues.username : '',
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
ng-model="$ctrl.model.RepositoryAuthentication"
|
checked="$ctrl.model.RepositoryAuthentication"
|
||||||
label="Authentication"
|
label="'Authentication'"
|
||||||
name="authSwitch"
|
name="'authSwitch'"
|
||||||
on-change="($ctrl.onChangeAuth)"
|
on-change="($ctrl.onChangeAuth)"
|
||||||
data-cy="component-gitAuthToggle"
|
data-cy="'component-gitAuthToggle'"
|
||||||
></por-switch-field>
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { FORCE_REDEPLOYMENT } from '@/portainer/feature-flags/feature-ids';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
class GitFormAutoUpdateFieldsetController {
|
class GitFormAutoUpdateFieldsetController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(clipboard) {
|
constructor($scope, clipboard) {
|
||||||
|
Object.assign(this, { $scope, clipboard });
|
||||||
|
|
||||||
this.onChangeAutoUpdate = this.onChangeField('RepositoryAutomaticUpdates');
|
this.onChangeAutoUpdate = this.onChangeField('RepositoryAutomaticUpdates');
|
||||||
this.onChangeMechanism = this.onChangeField('RepositoryMechanism');
|
this.onChangeMechanism = this.onChangeField('RepositoryMechanism');
|
||||||
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
|
this.onChangeInterval = this.onChangeField('RepositoryFetchInterval');
|
||||||
this.clipboard = clipboard;
|
|
||||||
|
|
||||||
this.limitedFeature = FORCE_REDEPLOYMENT;
|
this.limitedFeature = FeatureId.FORCE_REDEPLOYMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyWebhook() {
|
copyWebhook() {
|
||||||
|
@ -19,9 +20,11 @@ class GitFormAutoUpdateFieldsetController {
|
||||||
|
|
||||||
onChangeField(field) {
|
onChangeField(field) {
|
||||||
return (value) => {
|
return (value) => {
|
||||||
this.onChange({
|
this.$scope.$evalAsync(() => {
|
||||||
...this.model,
|
this.onChange({
|
||||||
[field]: value,
|
...this.model,
|
||||||
|
[field]: value,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<ng-form name="autoUpdateForm">
|
<ng-form name="autoUpdateForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field name="autoUpdate" ng-model="$ctrl.model.RepositoryAutomaticUpdates" label="Automatic Updates" on-change="($ctrl.onChangeAutoUpdate)"></por-switch-field>
|
<por-switch-field name="'autoUpdate'" checked="$ctrl.model.RepositoryAutomaticUpdates" label="'Automatic Updates'" on-change="($ctrl.onChangeAutoUpdate)"></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||||
|
@ -59,7 +59,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
<div class="form-group" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field name="forceUpdate" feature="$ctrl.limitedFeature" ng-model="$ctrl.model.RepositoryAutomaticUpdatesForce" label="Force Redeployment"></por-switch-field>
|
<por-switch-field
|
||||||
|
name="'forceUpdate'"
|
||||||
|
feature-id="$ctrl.limitedFeature"
|
||||||
|
checked="$ctrl.model.RepositoryAutomaticUpdatesForce"
|
||||||
|
label="'Force Redeployment'"
|
||||||
|
on-change="($ctrl.onChangeAutoUpdateForce)"
|
||||||
|
></por-switch-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
<div class="small text-warning" style="margin: 5px 0 10px 0;" ng-if="$ctrl.model.RepositoryAutomaticUpdates">
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<label style="display: flex; align-items: center; margin: 0;">
|
|
||||||
<span for="toggle_{{::$ctrl.name}}" class="control-label text-left space-right" ng-class="$ctrl.labelClass" style="padding: 0;">
|
|
||||||
{{::$ctrl.label}}
|
|
||||||
<portainer-tooltip ng-if="$ctrl.tooltip" position="bottom" message="{{::$ctrl.tooltip}}"></portainer-tooltip>
|
|
||||||
</span>
|
|
||||||
<por-switch
|
|
||||||
class-name="space-right"
|
|
||||||
name="toggle_{{::$ctrl.name}}"
|
|
||||||
id="toggle_{{::$ctrl.name}}"
|
|
||||||
ng-model="$ctrl.ngModel"
|
|
||||||
disabled="$ctrl.disabled"
|
|
||||||
on-change="($ctrl.onChange)"
|
|
||||||
feature="$ctrl.feature"
|
|
||||||
ng-data-cy="{{::$ctrl.ngDataCy}}"
|
|
||||||
></por-switch>
|
|
||||||
</label>
|
|
|
@ -1,18 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
export const porSwitchField = {
|
|
||||||
templateUrl: './por-switch-field.html',
|
|
||||||
bindings: {
|
|
||||||
tooltip: '@',
|
|
||||||
ngModel: '=',
|
|
||||||
label: '@',
|
|
||||||
name: '@',
|
|
||||||
labelClass: '@',
|
|
||||||
ngDataCy: '@',
|
|
||||||
disabled: '<',
|
|
||||||
onChange: '<',
|
|
||||||
feature: '<', // feature id
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('porSwitchField', porSwitchField);
|
|
|
@ -1,14 +0,0 @@
|
||||||
export default class PorSwitchController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(featureService) {
|
|
||||||
Object.assign(this, { featureService });
|
|
||||||
|
|
||||||
this.limitedToBE = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
if (this.feature) {
|
|
||||||
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
<label class="switch" ng-class="[$ctrl.className, { business: $ctrl.limitedToBE, limited: $ctrl.limitedToBE }]" style="margin-bottom: 0;">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="{{::$ctrl.name}}"
|
|
||||||
id="{{::$ctrl.id}}"
|
|
||||||
ng-model="$ctrl.ngModel"
|
|
||||||
ng-disabled="$ctrl.disabled || $ctrl.limitedToBE"
|
|
||||||
ng-change="$ctrl.onChange($ctrl.ngModel)"
|
|
||||||
/>
|
|
||||||
<i data-cy="{{::$ctrl.ngDataCy}}"></i>
|
|
||||||
</label>
|
|
||||||
<be-feature-indicator ng-if="$ctrl.limitedToBE" feature="$ctrl.feature"></be-feature-indicator>
|
|
|
@ -1,21 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import controller from './por-switch.controller';
|
|
||||||
|
|
||||||
import './por-switch.css';
|
|
||||||
|
|
||||||
const porSwitch = {
|
|
||||||
templateUrl: './por-switch.html',
|
|
||||||
controller,
|
|
||||||
bindings: {
|
|
||||||
ngModel: '=',
|
|
||||||
id: '@',
|
|
||||||
className: '@',
|
|
||||||
name: '@',
|
|
||||||
ngDataCy: '@',
|
|
||||||
disabled: '<',
|
|
||||||
onChange: '<',
|
|
||||||
feature: '<', // feature id
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
angular.module('portainer.app').component('porSwitch', porSwitch);
|
|
|
@ -1,16 +0,0 @@
|
||||||
angular.module('portainer.app').directive('rdHeaderContent', [
|
|
||||||
'Authentication',
|
|
||||||
function rdHeaderContent(Authentication) {
|
|
||||||
var directive = {
|
|
||||||
requires: '^rdHeader',
|
|
||||||
transclude: true,
|
|
||||||
link: function (scope) {
|
|
||||||
scope.username = Authentication.getUserDetails().username;
|
|
||||||
},
|
|
||||||
template:
|
|
||||||
'<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;" data-cy="template-logoutButton"><u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u></a></div></div>',
|
|
||||||
restrict: 'E',
|
|
||||||
};
|
|
||||||
return directive;
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,19 +0,0 @@
|
||||||
angular.module('portainer.app').directive('rdHeaderTitle', [
|
|
||||||
'Authentication',
|
|
||||||
function rdHeaderTitle(Authentication) {
|
|
||||||
var directive = {
|
|
||||||
requires: '^rdHeader',
|
|
||||||
scope: {
|
|
||||||
titleText: '@',
|
|
||||||
},
|
|
||||||
link: function (scope) {
|
|
||||||
scope.username = Authentication.getUserDetails().username;
|
|
||||||
},
|
|
||||||
transclude: true,
|
|
||||||
template:
|
|
||||||
'<div class="page white-space-normal">{{titleText}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle" aria-hidden="true"></i> {{username}}</span></div>',
|
|
||||||
restrict: 'E',
|
|
||||||
};
|
|
||||||
return directive;
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,11 +0,0 @@
|
||||||
angular.module('portainer.app').directive('rdHeader', function rdHeader() {
|
|
||||||
var directive = {
|
|
||||||
scope: {
|
|
||||||
ngModel: '=',
|
|
||||||
},
|
|
||||||
transclude: true,
|
|
||||||
template: '<div class="row header"><div id="loadingbar-placeholder"></div><div class="col-xs-12"><div class="meta" ng-transclude></div></div></div>',
|
|
||||||
restrict: 'EA',
|
|
||||||
};
|
|
||||||
return directive;
|
|
||||||
});
|
|
|
@ -7,12 +7,16 @@ import gitFormModule from './forms/git-form';
|
||||||
import porAccessManagementModule from './accessManagement';
|
import porAccessManagementModule from './accessManagement';
|
||||||
import formComponentsModule from './form-components';
|
import formComponentsModule from './form-components';
|
||||||
import widgetModule from './widget';
|
import widgetModule from './widget';
|
||||||
|
import boxSelectorModule from './BoxSelector';
|
||||||
|
import headerModule from './Header';
|
||||||
|
|
||||||
import { ReactExampleAngular } from './ReactExample';
|
import { ReactExampleAngular } from './ReactExample';
|
||||||
import { TooltipAngular } from './Tip/Tooltip';
|
import { TooltipAngular } from './Tip/Tooltip';
|
||||||
|
import { beFeatureIndicatorAngular } from './BEFeatureIndicator';
|
||||||
|
|
||||||
export default angular
|
export default angular
|
||||||
.module('portainer.app.components', [widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
|
.module('portainer.app.components', [headerModule, boxSelectorModule, widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule])
|
||||||
.component('portainerTooltip', TooltipAngular)
|
.component('portainerTooltip', TooltipAngular)
|
||||||
.component('reactExample', ReactExampleAngular)
|
.component('reactExample', ReactExampleAngular)
|
||||||
|
.component('beFeatureIndicator', beFeatureIndicatorAngular)
|
||||||
.component('createAccessToken', CreateAccessTokenAngular).name;
|
.component('createAccessToken', CreateAccessTokenAngular).name;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||||
|
|
||||||
export default class ThemeSettingsController {
|
export default class ThemeSettingsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -31,6 +31,7 @@ export default class ThemeSettingsController {
|
||||||
this.ThemeManager.setTheme(theme);
|
this.ThemeManager.setTheme(theme);
|
||||||
}
|
}
|
||||||
this.state.themeInProgress = true;
|
this.state.themeInProgress = true;
|
||||||
|
this.state.userTheme = theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
<rd-widget-header icon="fa-palette" title-text="Change user theme"></rd-widget-header>
|
<rd-widget-header icon="fa-palette" title-text="Change user theme"></rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="theme-panel">
|
<form class="theme-panel">
|
||||||
<!-- Theme -->
|
<!-- Theme Selector-->
|
||||||
<box-selector radio-name="theme" ng-model="$ctrl.state.userTheme" options="$ctrl.state.availableThemes" on-change="($ctrl.setTheme)"></box-selector>
|
<box-selector radio-name="'theme'" value="$ctrl.state.userTheme" options="$ctrl.state.availableThemes" on-change="($ctrl.setTheme)"></box-selector>
|
||||||
<button ng-click="$ctrl.updateTheme()" class="btn btn-primary btn-sm">Update theme</button>
|
<button ng-click="$ctrl.updateTheme()" class="btn btn-primary btn-sm">Update theme</button>
|
||||||
<!-- !Theme -->
|
<!-- !Theme -->
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export const EDITIONS = {
|
|
||||||
CE: 0,
|
|
||||||
BE: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STATES = {
|
|
||||||
HIDDEN: 0,
|
|
||||||
VISIBLE: 1,
|
|
||||||
LIMITED_BE: 2,
|
|
||||||
};
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
export enum Edition {
|
||||||
|
CE,
|
||||||
|
BE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FeatureState {
|
||||||
|
HIDDEN,
|
||||||
|
VISIBLE,
|
||||||
|
LIMITED_BE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FeatureId {
|
||||||
|
K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota',
|
||||||
|
K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota',
|
||||||
|
RBAC_ROLES = 'rbac-roles',
|
||||||
|
REGISTRY_MANAGEMENT = 'registry-management',
|
||||||
|
K8S_SETUP_DEFAULT = 'k8s-setup-default',
|
||||||
|
S3_BACKUP_SETTING = 's3-backup-setting',
|
||||||
|
HIDE_INTERNAL_AUTHENTICATION_PROMPT = 'hide-internal-authentication-prompt',
|
||||||
|
TEAM_MEMBERSHIP = 'team-membership',
|
||||||
|
HIDE_INTERNAL_AUTH = 'hide-internal-auth',
|
||||||
|
EXTERNAL_AUTH_LDAP = 'external-auth-ldap',
|
||||||
|
ACTIVITY_AUDIT = 'activity-audit',
|
||||||
|
FORCE_REDEPLOYMENT = 'force-redeployment',
|
||||||
|
HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window',
|
||||||
|
}
|
|
@ -1,59 +0,0 @@
|
||||||
import { EDITIONS, STATES } from './enums';
|
|
||||||
|
|
||||||
import * as FEATURE_IDS from './feature-ids';
|
|
||||||
|
|
||||||
export function featureService() {
|
|
||||||
const state = {
|
|
||||||
currentEdition: undefined,
|
|
||||||
features: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectShow,
|
|
||||||
init,
|
|
||||||
isLimitedToBE,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
// will be loaded on runtime
|
|
||||||
const currentEdition = EDITIONS.CE;
|
|
||||||
const features = {
|
|
||||||
[FEATURE_IDS.K8S_RESOURCE_POOL_LB_QUOTA]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.K8S_RESOURCE_POOL_STORAGE_QUOTA]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.ACTIVITY_AUDIT]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.EXTERNAL_AUTH_LDAP]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.HIDE_INTERNAL_AUTH]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.HIDE_INTERNAL_AUTHENTICATION_PROMPT]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.K8S_SETUP_DEFAULT]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.RBAC_ROLES]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.REGISTRY_MANAGEMENT]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.S3_BACKUP_SETTING]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.TEAM_MEMBERSHIP]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.HIDE_AUTO_UPDATE_WINDOW]: EDITIONS.BE,
|
|
||||||
[FEATURE_IDS.FORCE_REDEPLOYMENT]: EDITIONS.BE,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.currentEdition = currentEdition;
|
|
||||||
state.features = features;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectShow(featureId) {
|
|
||||||
if (!state.features[featureId]) {
|
|
||||||
return STATES.HIDDEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.features[featureId] <= state.currentEdition) {
|
|
||||||
return STATES.VISIBLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.features[featureId] === EDITIONS.BE) {
|
|
||||||
return STATES.LIMITED_BE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return STATES.HIDDEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLimitedToBE(featureId) {
|
|
||||||
return selectShow(featureId) === STATES.LIMITED_BE;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue