Compare commits
11 Commits
develop
...
feat/1-rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27e44f5f2 | ||
|
|
5f16799b4c | ||
|
|
28a06e80a8 | ||
|
|
90f51d48bb | ||
|
|
b1b09e5da0 | ||
|
|
b7df90905d | ||
|
|
ef47503bf8 | ||
|
|
76896e5916 | ||
|
|
7dc98df2b6 | ||
|
|
cddccd2a5f | ||
|
|
003a90c235 |
@@ -99,8 +99,6 @@
|
||||
|
||||
--orange-1: #e86925;
|
||||
|
||||
--BE-only: var(--ui-gray-6);
|
||||
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
|
||||
@@ -269,8 +267,6 @@
|
||||
|
||||
/* Dark Theme */
|
||||
[theme='dark'] {
|
||||
--BE-only: var(--ui-gray-6);
|
||||
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
|
||||
@@ -440,7 +436,6 @@
|
||||
|
||||
/* High Contrast Theme */
|
||||
[theme='highcontrast'] {
|
||||
--BE-only: var(--ui-gray-6);
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDeta
|
||||
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
import { InsightsBox } from '@/react/components/InsightsBox';
|
||||
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
|
||||
import { EventsDatatable } from '@/react/docker/events/EventsDatatables';
|
||||
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
|
||||
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
||||
@@ -59,7 +58,6 @@ const ngModule = angular
|
||||
'className',
|
||||
])
|
||||
)
|
||||
.component('betaAlert', r2a(BetaAlert, ['className', 'message', 'isHtml']))
|
||||
.component(
|
||||
'agentHostBrowserReact',
|
||||
r2a(withUIRouter(withCurrentUser(AgentHostBrowser)), [
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
export default class DockerFeaturesConfigurationController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, $state, EndpointService, SettingsService, Notifications, StateManager) {
|
||||
@@ -11,9 +9,6 @@ export default class DockerFeaturesConfigurationController {
|
||||
this.Notifications = Notifications;
|
||||
this.StateManager = StateManager;
|
||||
|
||||
this.limitedFeatureAutoUpdate = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||
this.limitedFeatureUpToDateImage = FeatureId.IMAGE_UP_TO_DATE_INDICATOR;
|
||||
|
||||
this.formValues = {
|
||||
enableHostManagementFeatures: false,
|
||||
allowVolumeBrowserForRegularUsers: false,
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable Change Window'"
|
||||
label-class="'col-sm-7 col-lg-4'"
|
||||
feature-id="$ctrl.limitedFeatureAutoUpdate"
|
||||
tooltip="'Specify a time-frame during which GitOps updates can occur in this environment.'"
|
||||
on-change="($ctrl.onToggleAutoUpdate)"
|
||||
>
|
||||
@@ -202,7 +201,6 @@
|
||||
checked="false"
|
||||
name="'outOfDateImageToggle'"
|
||||
label-class="'col-sm-7 col-lg-4'"
|
||||
feature-id="$ctrl.limitedFeatureUpToDateImage"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -128,27 +128,6 @@ angular
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'edge.devices',
|
||||
url: '/devices',
|
||||
abstract: true,
|
||||
});
|
||||
|
||||
if (process.env.PORTAINER_EDITION === 'BE') {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'edge.devices.waiting-room',
|
||||
url: '/waiting-room',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'waitingRoomView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
docs: '/user/edge/waiting-room',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'edge.templates',
|
||||
url: '/templates?template',
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { WaitingRoomView } from '@/react/edge/edge-devices/WaitingRoomView';
|
||||
|
||||
import { templatesModule } from './templates';
|
||||
import { jobsModule } from './jobs';
|
||||
import { stacksModule } from './edge-stacks';
|
||||
import { groupsModule } from './groups';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.edge.react.views', [
|
||||
templatesModule,
|
||||
jobsModule,
|
||||
stacksModule,
|
||||
groupsModule,
|
||||
])
|
||||
.component(
|
||||
'waitingRoomView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(WaitingRoomView))), [])
|
||||
).name;
|
||||
export const viewsModule = angular.module('portainer.edge.react.views', [
|
||||
templatesModule,
|
||||
jobsModule,
|
||||
stacksModule,
|
||||
groupsModule,
|
||||
]).name;
|
||||
|
||||
10
app/global.d.ts
vendored
10
app/global.d.ts
vendored
@@ -76,13 +76,3 @@ interface Window {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
declare module 'process' {
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PORTAINER_EDITION: 'BE' | 'CE';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" ng-app="portainer" ng-strict-di data-edition="<%= process.env.PORTAINER_EDITION %>">
|
||||
<html lang="en" ng-app="portainer" ng-strict-di>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Portainer</title>
|
||||
|
||||
@@ -5,9 +5,6 @@ import './i18n';
|
||||
import angular from 'angular';
|
||||
import { UI_ROUTER_REACT_HYBRID } from '@uirouter/react-hybrid';
|
||||
|
||||
import { Edition } from '@/react/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import './agent';
|
||||
import { azureModule } from './azure';
|
||||
import './docker/__module';
|
||||
@@ -30,8 +27,6 @@ if (window.origin == 'http://localhost:49000') {
|
||||
document.getElementById('base').href = basePath;
|
||||
}
|
||||
|
||||
initFeatureService(Edition[process.env.PORTAINER_EDITION]);
|
||||
|
||||
angular
|
||||
.module('portainer', [
|
||||
'ui.bootstrap',
|
||||
|
||||
@@ -205,10 +205,6 @@
|
||||
</div>
|
||||
<!-- #end region IMAGE FIELD -->
|
||||
|
||||
<div class="col-sm-12 mb-4 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.formValues.ResourcePool">
|
||||
<!-- #region STACK -->
|
||||
<kube-stack-name
|
||||
|
||||
@@ -76,10 +76,6 @@
|
||||
</div>
|
||||
<!-- !name -->
|
||||
|
||||
<div class="col-sm-12 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.formValues.ResourcePool">
|
||||
<kubernetes-configuration-data
|
||||
ng-if="ctrl.formValues"
|
||||
|
||||
@@ -81,10 +81,6 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||
<div class="col-sm-12 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
<kubernetes-configuration-data
|
||||
ng-if="ctrl.formValues"
|
||||
form-values="ctrl.formValues"
|
||||
|
||||
@@ -77,10 +77,6 @@
|
||||
</div>
|
||||
<!-- !name -->
|
||||
|
||||
<div class="col-sm-12 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.formValues.ResourcePool">
|
||||
<div class="col-sm-12 form-section-title"> Information </div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -32,10 +32,6 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||
<div class="col-sm-12 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
</div>
|
||||
|
||||
<kubernetes-configuration-data
|
||||
ng-if="ctrl.formValues"
|
||||
form-values="ctrl.formValues"
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
checked="formValues.enabled"
|
||||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable pod security constraints'"
|
||||
feature-id="limitedFeaturePodSecurityPolicy"
|
||||
label-class="'col-sm-3 col-lg-2 px-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import angular from 'angular';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
angular.module('portainer.kubernetes').controller('KubernetesSecurityConstraintController', [
|
||||
'$scope',
|
||||
'EndpointProvider',
|
||||
'EndpointService',
|
||||
function ($scope, EndpointProvider, EndpointService) {
|
||||
$scope.limitedFeaturePodSecurityPolicy = FeatureId.POD_SECURITY_POLICY_CONSTRAINT;
|
||||
$scope.state = {
|
||||
viewReady: false,
|
||||
actionInProgress: false,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import featureFlagModule from '@/react/portainer/feature-flags';
|
||||
|
||||
import './rbac';
|
||||
|
||||
import componentsModule from './components';
|
||||
@@ -24,7 +22,6 @@ angular
|
||||
'portainer.registrymanagement',
|
||||
componentsModule,
|
||||
settingsModule,
|
||||
featureFlagModule,
|
||||
userActivityModule,
|
||||
servicesModule,
|
||||
reactModule,
|
||||
@@ -201,19 +198,6 @@ angular
|
||||
},
|
||||
};
|
||||
|
||||
const edgeAutoCreateScript = {
|
||||
name: 'portainer.endpoints.edgeAutoCreateScript',
|
||||
url: '/aeec',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeAutoCreateScriptView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
docs: '/admin/environments/aeec',
|
||||
},
|
||||
};
|
||||
|
||||
var endpointAccess = {
|
||||
name: 'portainer.endpoints.endpoint.access',
|
||||
url: '/access',
|
||||
@@ -471,7 +455,6 @@ angular
|
||||
$stateRegistryProvider.register(endpoints);
|
||||
$stateRegistryProvider.register(endpoint);
|
||||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(edgeAutoCreateScript);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupCreation);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<a class="vertical-center be-indicator ml-5" href="{{ $ctrl.url }}" target="_blank" rel="noopener" ng-if="$ctrl.limitedToBE">
|
||||
<ng-transclude></ng-transclude>
|
||||
<pr-icon icon="'briefcase'" class-name="'space-right be-indicator-icon'"></pr-icon>
|
||||
<span class="be-indicator-label">Business Feature</span>
|
||||
</a>
|
||||
@@ -1,10 +0,0 @@
|
||||
import controller from './BEFeatureIndicator.controller';
|
||||
|
||||
export const beFeatureIndicator = {
|
||||
templateUrl: './BEFeatureIndicator.html',
|
||||
controller,
|
||||
bindings: {
|
||||
feature: '<',
|
||||
},
|
||||
transclude: true,
|
||||
};
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
IComponentOptions,
|
||||
IComponentController,
|
||||
IFormController,
|
||||
IScope,
|
||||
} from 'angular';
|
||||
import { IComponentOptions, IComponentController, IScope } from 'angular';
|
||||
|
||||
class BoxSelectorController implements IComponentController {
|
||||
formCtrl!: IFormController;
|
||||
|
||||
onChange!: (value: string | number) => void;
|
||||
|
||||
radioName!: string;
|
||||
@@ -21,9 +14,8 @@ class BoxSelectorController implements IComponentController {
|
||||
this.$scope = $scope;
|
||||
}
|
||||
|
||||
handleChange(value: string | number, limitedToBE: boolean) {
|
||||
handleChange(value: string | number) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formCtrl.$setValidity(this.radioName, !limitedToBE, this.formCtrl);
|
||||
this.onChange(value);
|
||||
});
|
||||
}
|
||||
@@ -46,8 +38,5 @@ export const BoxSelectorAngular: IComponentOptions = {
|
||||
slim: '<',
|
||||
label: '<',
|
||||
},
|
||||
require: {
|
||||
formCtrl: '^form',
|
||||
},
|
||||
controller: BoxSelectorController,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector/types';
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
@@ -8,8 +6,7 @@ export function buildOption<T extends number | string>(
|
||||
icon: IconProps['icon'],
|
||||
label: BoxSelectorOption<T>['label'],
|
||||
description: BoxSelectorOption<T>['description'],
|
||||
value: BoxSelectorOption<T>['value'],
|
||||
feature?: FeatureId
|
||||
value: BoxSelectorOption<T>['value']
|
||||
): BoxSelectorOption<T> {
|
||||
return { id, icon, label, description, value, feature };
|
||||
return { id, icon, label, description, value };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,5 @@ export const porAccessManagement = {
|
||||
updateAccess: '<',
|
||||
actionInProgress: '<',
|
||||
filterUsers: '<',
|
||||
limitedFeature: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,11 +28,10 @@
|
||||
class="form-control"
|
||||
data-cy="access-management-role-select"
|
||||
ng-model="ctrl.formValues.selectedRole"
|
||||
ng-options="role as ctrl.roleLabel(role) disable when ctrl.isRoleLimitedToBE(role) for role in ctrl.roles"
|
||||
ng-options="role as role.Name for role in ctrl.roles"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
<be-feature-indicator feature="ctrl.limitedFeature" class="space-left"></be-feature-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,13 +65,8 @@
|
||||
<access-datatable
|
||||
ng-if="ctrl.authorizedUsersAndTeams"
|
||||
table-key="'access_' + ctrl.entityType"
|
||||
show-warning="ctrl.entityType !== 'registry'"
|
||||
is-update-enabled="ctrl.entityType !== 'registry'"
|
||||
show-roles="ctrl.entityType !== 'registry'"
|
||||
roles="ctrl.roles"
|
||||
inherit-from="ctrl.inheritFrom"
|
||||
dataset="ctrl.authorizedUsersAndTeams"
|
||||
on-update="(ctrl.updateAction)"
|
||||
on-remove="(ctrl.unauthorizeAccess)"
|
||||
>
|
||||
</access-datatable>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
|
||||
import { RoleTypes } from '@/portainer/rbac/models/role';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
class PorAccessManagementController {
|
||||
/* @ngInject */
|
||||
constructor($scope, $state, Notifications, AccessService, RoleService) {
|
||||
Object.assign(this, { $scope, $state, Notifications, AccessService, RoleService });
|
||||
|
||||
this.limitedToBE = false;
|
||||
this.$state = $state;
|
||||
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
this.updateAction = this.updateAction.bind(this);
|
||||
this.onChangeUsersAndTeams = this.onChangeUsersAndTeams.bind(this);
|
||||
}
|
||||
|
||||
@@ -23,17 +18,6 @@ class PorAccessManagementController {
|
||||
});
|
||||
}
|
||||
|
||||
updateAction(updatedUserAccesses, updatedTeamAccesses) {
|
||||
const entity = this.accessControlledEntity;
|
||||
const oldUserAccessPolicies = entity.UserAccessPolicies;
|
||||
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
|
||||
|
||||
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, updatedUserAccesses, updatedTeamAccesses);
|
||||
this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies;
|
||||
this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
|
||||
this.updateAccess();
|
||||
}
|
||||
|
||||
authorizeAccess() {
|
||||
const entity = this.accessControlledEntity;
|
||||
const oldUserAccessPolicies = entity.UserAccessPolicies;
|
||||
@@ -60,32 +44,8 @@ class PorAccessManagementController {
|
||||
this.updateAccess();
|
||||
}
|
||||
|
||||
isRoleLimitedToBE(role) {
|
||||
if (!this.limitedToBE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return role.ID !== RoleTypes.STANDARD;
|
||||
}
|
||||
|
||||
roleLabel(role) {
|
||||
if (!this.limitedToBE) {
|
||||
return role.Name;
|
||||
}
|
||||
|
||||
if (this.isRoleLimitedToBE(role)) {
|
||||
return `${role.Name} (Business Feature)`;
|
||||
}
|
||||
|
||||
return `${role.Name} (Default)`;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
if (this.limitedFeature) {
|
||||
this.limitedToBE = isLimitedToBE(this.limitedFeature);
|
||||
}
|
||||
|
||||
const entity = this.accessControlledEntity;
|
||||
const parent = this.inheritFrom;
|
||||
|
||||
@@ -93,7 +53,7 @@ class PorAccessManagementController {
|
||||
this.roles = _.orderBy(roles, 'Priority', 'asc');
|
||||
this.formValues = {
|
||||
multiselectOutput: [],
|
||||
selectedRole: this.roles.find((role) => !this.isRoleLimitedToBE(role)),
|
||||
selectedRole: this.roles[0],
|
||||
};
|
||||
|
||||
const data = await this.AccessService.accesses(entity, parent, this.roles);
|
||||
|
||||
@@ -5,12 +5,10 @@ import porAccessManagementModule from './accessManagement';
|
||||
import widgetModule from './widget';
|
||||
import { boxSelectorModule } from './BoxSelector';
|
||||
|
||||
import { beFeatureIndicator } from './BEFeatureIndicator';
|
||||
import { InformationPanelAngular } from './InformationPanel';
|
||||
import { gitFormModule } from './forms/git-form';
|
||||
import { tlsFieldsetModule } from './tls-fieldset';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.components', [boxSelectorModule, widgetModule, gitFormModule, porAccessManagementModule, formComponentsModule, tlsFieldsetModule])
|
||||
.component('informationPanel', InformationPanelAngular)
|
||||
.component('beFeatureIndicator', beFeatureIndicator).name;
|
||||
.component('informationPanel', InformationPanelAngular).name;
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import providers, { getProviderByUrl } from './providers';
|
||||
|
||||
const MS_TENANT_ID_PLACEHOLDER = 'TENANT_ID';
|
||||
@@ -13,9 +8,6 @@ export default class OAuthSettingsController {
|
||||
constructor($scope, $async) {
|
||||
Object.assign(this, { $scope, $async });
|
||||
|
||||
this.limitedFeature = FeatureId.HIDE_INTERNAL_AUTH;
|
||||
this.limitedFeatureClass = 'limited-be';
|
||||
|
||||
this.state = {
|
||||
provider: 'custom',
|
||||
overrideConfiguration: false,
|
||||
@@ -27,9 +19,6 @@ export default class OAuthSettingsController {
|
||||
this.onMicrosoftTenantIDChange = this.onMicrosoftTenantIDChange.bind(this);
|
||||
this.useDefaultProviderConfiguration = this.useDefaultProviderConfiguration.bind(this);
|
||||
this.updateSSO = this.updateSSO.bind(this);
|
||||
this.addTeamMembershipMapping = this.addTeamMembershipMapping.bind(this);
|
||||
this.removeTeamMembership = this.removeTeamMembership.bind(this);
|
||||
this.onToggleAutoTeamMembership = this.onToggleAutoTeamMembership.bind(this);
|
||||
this.onChangeAuthStyle = this.onChangeAuthStyle.bind(this);
|
||||
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
|
||||
}
|
||||
@@ -54,21 +43,16 @@ export default class OAuthSettingsController {
|
||||
|
||||
this.state.overrideConfiguration = false;
|
||||
|
||||
if (!this.isLimitedToBE || providerId === 'custom') {
|
||||
this.settings.AuthorizationURI = provider.authUrl;
|
||||
this.settings.AccessTokenURI = provider.accessTokenUrl;
|
||||
this.settings.ResourceURI = provider.resourceUrl;
|
||||
this.settings.LogoutURI = provider.logoutUrl;
|
||||
this.settings.UserIdentifier = provider.userIdentifier;
|
||||
this.settings.Scopes = provider.scopes;
|
||||
this.settings.AuthStyle = provider.authStyle;
|
||||
this.settings.AuthorizationURI = provider.authUrl;
|
||||
this.settings.AccessTokenURI = provider.accessTokenUrl;
|
||||
this.settings.ResourceURI = provider.resourceUrl;
|
||||
this.settings.LogoutURI = provider.logoutUrl;
|
||||
this.settings.UserIdentifier = provider.userIdentifier;
|
||||
this.settings.Scopes = provider.scopes;
|
||||
this.settings.AuthStyle = provider.authStyle;
|
||||
|
||||
if (providerId === 'microsoft' && this.state.microsoftTenantID !== '') {
|
||||
this.onMicrosoftTenantIDChange();
|
||||
}
|
||||
} else {
|
||||
this.settings.ClientID = '';
|
||||
this.settings.ClientSecret = '';
|
||||
if (providerId === 'microsoft' && this.state.microsoftTenantID !== '') {
|
||||
this.onMicrosoftTenantIDChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +65,6 @@ export default class OAuthSettingsController {
|
||||
updateSSO(checked) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.settings.SSO = checked;
|
||||
this.settings.HideInternalAuth = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,64 +74,7 @@ export default class OAuthSettingsController {
|
||||
});
|
||||
}
|
||||
|
||||
async onChangeHideInternalAuth(checked) {
|
||||
this.$async(async () => {
|
||||
if (this.isLimitedToBE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Hide internal authentication prompt',
|
||||
message: 'By hiding internal authentication prompt, you will only be able to login via SSO. Are you sure?',
|
||||
confirmButton: buildConfirmButton('Confirm', 'danger'),
|
||||
modalType: ModalType.Warn,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.settings.HideInternalAuth = checked;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleAutoTeamMembership(checked) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.settings.OAuthAutoMapTeamMemberships = checked;
|
||||
});
|
||||
}
|
||||
|
||||
addTeamMembershipMapping() {
|
||||
this.settings.TeamMemberships.OAuthClaimMappings.push({ ClaimValRegex: '', Team: this.settings.DefaultTeamID });
|
||||
}
|
||||
|
||||
removeTeamMembership(index) {
|
||||
this.settings.TeamMemberships.OAuthClaimMappings.splice(index, 1);
|
||||
}
|
||||
|
||||
isOAuthTeamMembershipFormValid() {
|
||||
if (this.settings.OAuthAutoMapTeamMemberships && this.settings.TeamMemberships) {
|
||||
if (!this.settings.TeamMemberships.OAuthClaimName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasInvalidMapping = this.settings.TeamMemberships.OAuthClaimMappings.some((m) => !(m.ClaimValRegex && m.Team));
|
||||
if (hasInvalidMapping) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.isLimitedToBE = isLimitedToBE(this.limitedFeature);
|
||||
|
||||
if (this.isLimitedToBE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.RedirectURI === '') {
|
||||
this.settings.RedirectURI = window.location.origin + baseHref();
|
||||
}
|
||||
@@ -169,13 +95,5 @@ export default class OAuthSettingsController {
|
||||
if (this.settings.DefaultTeamID === 0) {
|
||||
this.settings.DefaultTeamID = null;
|
||||
}
|
||||
|
||||
if (this.settings.TeamMemberships == null) {
|
||||
this.settings.TeamMemberships = {};
|
||||
}
|
||||
|
||||
if (this.settings.TeamMemberships.OAuthClaimMappings === null) {
|
||||
this.settings.TeamMemberships.OAuthClaimMappings = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,20 +15,6 @@
|
||||
</div>
|
||||
<!-- !SSO -->
|
||||
|
||||
<!-- HideInternalAuth -->
|
||||
<div class="form-group" ng-if="$ctrl.settings.SSO">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label="'Hide internal authentication prompt'"
|
||||
name="'hide-internal-auth'"
|
||||
feature-id="$ctrl.limitedFeature"
|
||||
checked="$ctrl.settings.HideInternalAuth"
|
||||
on-change="($ctrl.onChangeHideInternalAuth)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !HideInternalAuth -->
|
||||
|
||||
<auto-user-provision-toggle
|
||||
value="$ctrl.settings.OAuthAutoCreateUsers"
|
||||
on-change="($ctrl.onAutoUserProvisionChange)"
|
||||
@@ -52,16 +38,9 @@
|
||||
</span>
|
||||
|
||||
<div class="col-sm-9" ng-if="$ctrl.teams.length > 0">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
The default team option will be disabled when automatic team membership is enabled
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-xs-12 vertical-center">
|
||||
<select
|
||||
class="form-control"
|
||||
ng-disabled="$ctrl.settings.OAuthAutoMapTeamMemberships"
|
||||
ng-model="$ctrl.settings.DefaultTeamID"
|
||||
ng-options="team.Id as team.Name for team in $ctrl.teams"
|
||||
data-cy="default-team-select"
|
||||
@@ -72,7 +51,7 @@
|
||||
type="button"
|
||||
class="btn btn-md btn-danger"
|
||||
ng-click="$ctrl.settings.DefaultTeamID = null"
|
||||
ng-disabled="!$ctrl.settings.DefaultTeamID || $ctrl.settings.OAuthAutoMapTeamMemberships"
|
||||
ng-disabled="!$ctrl.settings.DefaultTeamID"
|
||||
ng-if="$ctrl.teams.length > 0"
|
||||
>
|
||||
<pr-icon icon="'x'" size="'md'"></pr-icon>
|
||||
@@ -82,99 +61,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Team membership </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small"> Automatic team membership synchronizes the team membership based on a custom claim in the token from the OAuth provider. </div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label="'Automatic team membership'"
|
||||
name="'tls'"
|
||||
feature-id="$ctrl.limitedFeature"
|
||||
checked="$ctrl.settings.OAuthAutoMapTeamMemberships"
|
||||
on-change="($ctrl.onToggleAutoTeamMembership)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.settings.OAuthAutoMapTeamMemberships">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="oauth_token_claim_name">
|
||||
Claim name
|
||||
<portainer-tooltip message="'The OpenID Connect UserInfo Claim name that contains the team identifier the user belongs to.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<div class="col-xs-11 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="oauth_token_claim_name"
|
||||
ng-model="$ctrl.settings.TeamMemberships.OAuthClaimName"
|
||||
placeholder="groups"
|
||||
data-cy="oauth-token-claim-name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left"> Statically assigned teams </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<span class="label label-default interactive vertical-center ml-4" ng-click="$ctrl.addTeamMembershipMapping()">
|
||||
<pr-icon icon="'plus-circle'"></pr-icon>
|
||||
add team mapping
|
||||
</span>
|
||||
|
||||
<div class="col-sm-12 form-inline" ng-repeat="mapping in $ctrl.settings.TeamMemberships.OAuthClaimMappings" style="margin-top: 0.75em">
|
||||
<div class="input-group input-group-sm col-sm-5">
|
||||
<span class="input-group-addon">claim value regex</span>
|
||||
<input type="text" class="form-control" ng-model="mapping.ClaimValRegex" data-cy="claim-value-regex" />
|
||||
</div>
|
||||
<span style="margin: 0px 0.5em">maps to</span>
|
||||
<div class="input-group input-group-sm col-sm-3 col-lg-4">
|
||||
<span class="input-group-addon">team</span>
|
||||
<select
|
||||
class="form-control"
|
||||
data-cy="team-select"
|
||||
ng-init="mapping.Team = mapping.Team || $ctrl.settings.DefaultTeamID"
|
||||
ng-model="mapping.Team"
|
||||
ng-options="team.Id as team.Name for team in $ctrl.teams"
|
||||
>
|
||||
<option selected value="">Select a team</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-md btn-danger" ng-click="$ctrl.removeTeamMembership($index)">
|
||||
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div class="small text-warning vertical-center mt-1" ng-show="!mapping.ClaimValRegex">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
Claim value regex is required.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small" style="margin-bottom: 0.5em"> The default team will be assigned when the user does not belong to any other team </div>
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
|
||||
<span class="small text-muted" style="margin-left: 20px" ng-if="$ctrl.teams.length === 0">
|
||||
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
|
||||
</span>
|
||||
|
||||
<div class="col-sm-9" ng-if="$ctrl.teams.length > 0">
|
||||
<div class="col-xs-11">
|
||||
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams" data-cy="default-team-select">
|
||||
<option value="">No team</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<oauth-providers-selector on-change="($ctrl.onSelectProvider)" value="$ctrl.state.provider"></oauth-providers-selector>
|
||||
|
||||
<div ng-if="$ctrl.state.provider == 'custom' || $ctrl.state.overrideConfiguration">
|
||||
@@ -191,9 +77,7 @@
|
||||
id="oauth_client_id"
|
||||
ng-model="$ctrl.settings.ClientID"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,9 +92,6 @@
|
||||
ng-model="$ctrl.settings.ClientSecret"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
autocomplete="new-password"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,8 +109,6 @@
|
||||
id="oauth_authorization_uri"
|
||||
ng-model="$ctrl.settings.AuthorizationURI"
|
||||
placeholder="https://example.com/oauth/authorize"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,8 +126,6 @@
|
||||
id="oauth_access_token_uri"
|
||||
ng-model="$ctrl.settings.AccessTokenURI"
|
||||
placeholder="https://example.com/oauth/token"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,8 +143,6 @@
|
||||
id="oauth_resource_uri"
|
||||
ng-model="$ctrl.settings.ResourceURI"
|
||||
placeholder="https://example.com/user"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,8 +162,6 @@
|
||||
id="oauth_redirect_uri"
|
||||
ng-model="$ctrl.settings.RedirectURI"
|
||||
placeholder="http://yourportainer.com/"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,8 +180,6 @@
|
||||
class="form-control"
|
||||
id="oauth_logout_url"
|
||||
ng-model="$ctrl.settings.LogoutURI"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,8 +199,6 @@
|
||||
id="oauth_user_identifier"
|
||||
ng-model="$ctrl.settings.UserIdentifier"
|
||||
placeholder="id"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,8 +218,6 @@
|
||||
id="oauth_scopes"
|
||||
ng-model="$ctrl.settings.Scopes"
|
||||
placeholder="id,email,name"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,97 +226,9 @@
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="!$ctrl.isOAuthTeamMembershipFormValid() || oauthSettingsForm.$invalid"
|
||||
limited-feature-id="$ctrl.limitedFeature"
|
||||
limited-feature-class="$ctrl.limitedFeatureClass"
|
||||
save-button-disabled="oauthSettingsForm.$invalid"
|
||||
class-name="'oauth-save-settings-button'"
|
||||
></save-auth-settings-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.state.provider != 'custom'" class="limited-be be-indicator-container">
|
||||
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeature"></be-feature-indicator></div>
|
||||
<div class="limited-be-content">
|
||||
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.state.provider == 'microsoft'">
|
||||
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Tenant ID
|
||||
<portainer-tooltip message="'ID of the Azure Directory you wish to authenticate against. Also known as the Directory ID'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="oauth-microsoft-tenant-id"
|
||||
class="form-control"
|
||||
id="oauth_microsoft_tenant_id"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
ng-model="$ctrl.state.microsoftTenantID"
|
||||
ng-change="$ctrl.onMicrosoftTenantIDChange()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeature}}"
|
||||
limited-feature-class="limited-be"
|
||||
limited-feature-disabled
|
||||
limited-feature-tabindex="-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
{{ $ctrl.state.provider == 'microsoft' ? 'Application ID' : 'Client ID' }}
|
||||
<portainer-tooltip message="'Public identifier of the OAuth application'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="oauth-client-id"
|
||||
id="oauth_client_id"
|
||||
ng-model="$ctrl.settings.ClientID"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxx"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left"> {{ $ctrl.state.provider == 'microsoft' ? 'Application key' : 'Client secret' }} </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="oauth_client_secret"
|
||||
ng-model="$ctrl.settings.ClientSecret"
|
||||
placeholder="*******"
|
||||
autocomplete="new-password"
|
||||
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
|
||||
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
|
||||
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive vertical-center" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
|
||||
<pr-icon icon="'wrench'"></pr-icon>
|
||||
Override default configuration
|
||||
</a>
|
||||
<a class="small interactive vertical-center" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.useDefaultProviderConfiguration($ctrl.state.provider)">
|
||||
<pr-icon icon="'settings'"></pr-icon>
|
||||
Use default configuration
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="!$ctrl.isOAuthTeamMembershipFormValid() || oauthSettingsForm.$invalid"
|
||||
limited-feature-id="$ctrl.limitedFeature"
|
||||
limited-feature-class="$ctrl.limitedFeatureClass"
|
||||
class-name="'oauth-save-settings-button'"
|
||||
></save-auth-settings-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
export default class AccessViewerController {
|
||||
/* @ngInject */
|
||||
constructor($scope, Notifications, UserService, TeamMembershipService, Authentication) {
|
||||
this.$scope = $scope;
|
||||
this.Notifications = Notifications;
|
||||
this.UserService = UserService;
|
||||
this.TeamMembershipService = TeamMembershipService;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.limitedFeature = 'rbac-roles';
|
||||
this.users = [];
|
||||
this.selectedUserId = null;
|
||||
|
||||
this.onUserSelect = this.onUserSelect.bind(this);
|
||||
}
|
||||
|
||||
onUserSelect(selectedUserId) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.selectedUserId = selectedUserId;
|
||||
});
|
||||
}
|
||||
|
||||
// for admin, returns all users
|
||||
// for team leader, only return all his/her team member users
|
||||
async teamMemberUsers(users, teamMemberships) {
|
||||
if (this.isAdmin) {
|
||||
return users;
|
||||
}
|
||||
|
||||
const filteredUsers = [];
|
||||
const userId = this.Authentication.getUserDetails().ID;
|
||||
const leadingTeams = await this.UserService.userLeadingTeams(userId);
|
||||
|
||||
const isMember = (userId, teamId) => {
|
||||
return !!_.find(teamMemberships, { UserId: userId, TeamId: teamId });
|
||||
};
|
||||
|
||||
for (const user of users) {
|
||||
for (const leadingTeam of leadingTeams) {
|
||||
if (isMember(user.Id, leadingTeam.Id)) {
|
||||
filteredUsers.push(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
if (isLimitedToBE(this.limitedFeature)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
const allUsers = await this.UserService.users();
|
||||
const teamMemberships = await this.TeamMembershipService.memberships();
|
||||
const teamUsers = await this.teamMemberUsers(allUsers, teamMemberships);
|
||||
this.users = teamUsers.map((user) => ({ label: user.Username, value: user.Id }));
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve users');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<div class="col-sm-12 !mb-4">
|
||||
<div class="be-indicator-container limited-be">
|
||||
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeature"></be-feature-indicator></div>
|
||||
<div class="limited-be-content">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="user-x">
|
||||
<header-title> Effective access viewer </header-title>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title"> User </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted" ng-if="$ctrl.users.length === 0"> No user available </span>
|
||||
|
||||
<por-select ng-if="$ctrl.users.length > 0" value="$ctrl.selectedUserId" options="$ctrl.users" on-change="($ctrl.onUserSelect)" placeholder="'Select a user'">
|
||||
</por-select>
|
||||
</div>
|
||||
</div>
|
||||
<effective-access-viewer user-id="$ctrl.selectedUserId"></effective-access-viewer>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
import controller from './access-viewer.controller';
|
||||
|
||||
export const accessViewer = {
|
||||
templateUrl: './access-viewer.html',
|
||||
controller,
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AccessHeaders } from '../authorization-guard';
|
||||
import { rolesView } from './views/roles';
|
||||
import { accessViewer } from './components/access-viewer';
|
||||
|
||||
import { RoleService } from './services/role.service';
|
||||
import { RolesFactory } from './services/role.rest';
|
||||
@@ -8,7 +7,6 @@ import { RolesFactory } from './services/role.rest';
|
||||
angular
|
||||
.module('portainer.rbac', ['ngResource'])
|
||||
.constant('API_ENDPOINT_ROLES', 'api/roles')
|
||||
.component('accessViewer', accessViewer)
|
||||
.component('rolesView', rolesView)
|
||||
.factory('RoleService', RoleService)
|
||||
.factory('Roles', RolesFactory)
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<page-header title="'Roles'" breadcrumbs="['Role management']" reload="true"> </page-header>
|
||||
|
||||
<rbac-roles-datatable dataset="$ctrl.roles"></rbac-roles-datatable>
|
||||
|
||||
<div class="row">
|
||||
<access-viewer> </access-viewer>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { r2a } from '@/react-tools/react2angular';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { AnnotationsBeTeaser } from '@/react/kubernetes/annotations/AnnotationsBeTeaser';
|
||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
|
||||
@@ -32,7 +31,6 @@ import { Terminal } from '@@/Terminal/Terminal';
|
||||
import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||
import { Slider } from '@@/form-components/Slider';
|
||||
import { TagButton } from '@@/TagButton';
|
||||
import { BETeaserButton } from '@@/BETeaserButton';
|
||||
import { CodeEditor } from '@@/CodeEditor';
|
||||
import { HelpLink } from '@@/HelpLink';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
@@ -77,18 +75,6 @@ export const ngModule = angular
|
||||
'errors',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'beTeaserButton',
|
||||
r2a(BETeaserButton, [
|
||||
'featureId',
|
||||
'heading',
|
||||
'message',
|
||||
'buttonText',
|
||||
'className',
|
||||
'buttonClassName',
|
||||
'data-cy',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'tagButton',
|
||||
r2a(TagButton, ['value', 'label', 'title', 'onRemove'])
|
||||
@@ -261,7 +247,6 @@ export const ngModule = angular
|
||||
'inlineLoader',
|
||||
r2a(InlineLoader, ['children', 'className', 'size'])
|
||||
)
|
||||
.component('annotationsBeTeaser', r2a(AnnotationsBeTeaser, []))
|
||||
.component(
|
||||
'shellTerminal',
|
||||
r2a(Terminal, [
|
||||
|
||||
@@ -12,13 +12,8 @@ export const rbacModule = angular
|
||||
r2a(withUIRouter(withReactQuery(AccessDatatable)), [
|
||||
'dataset',
|
||||
'inheritFrom',
|
||||
'isUpdateEnabled',
|
||||
'onRemove',
|
||||
'onUpdate',
|
||||
'showRoles',
|
||||
'showWarning',
|
||||
'tableKey',
|
||||
'isUpdatingAccess',
|
||||
'isLoading',
|
||||
])
|
||||
).name;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { LDAPUsersTable } from '@/react/portainer/settings/AuthenticationView/LD
|
||||
import { LDAPGroupsTable } from '@/react/portainer/settings/AuthenticationView/LDAPAuth/LDAPGroupsTable';
|
||||
import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsView/ApplicationSettingsPanel';
|
||||
import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeSettingsPanel';
|
||||
import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel';
|
||||
import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel';
|
||||
import { SSLSettingsPanelWrapper } from '@/react/portainer/settings/SettingsView/SSLSettingsPanel/SSLSettingsPanel';
|
||||
import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OAuth';
|
||||
@@ -40,7 +39,7 @@ export const settingsModule = angular
|
||||
.component('ldapGroupsDatatable', r2a(LDAPGroupsTable, ['dataset']))
|
||||
.component(
|
||||
'ldapSettingsDnBuilder',
|
||||
r2a(DnBuilder, ['value', 'onChange', 'suffix', 'label', 'limitedFeatureId'])
|
||||
r2a(DnBuilder, ['value', 'onChange', 'suffix', 'label'])
|
||||
)
|
||||
.component(
|
||||
'ldapSettingsGroupDnBuilder',
|
||||
@@ -50,7 +49,6 @@ export const settingsModule = angular
|
||||
'suffix',
|
||||
'index',
|
||||
'onRemoveClick',
|
||||
'limitedFeatureId',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
@@ -61,7 +59,6 @@ export const settingsModule = angular
|
||||
'sslSettingsPanel',
|
||||
r2a(withReactQuery(SSLSettingsPanelWrapper), [])
|
||||
)
|
||||
.component('helmCertPanel', r2a(withReactQuery(HelmCertPanel), []))
|
||||
.component(
|
||||
'hiddenContainersPanel',
|
||||
r2a(withUIRouter(withReactQuery(HiddenContainersPanel)), [])
|
||||
@@ -92,12 +89,7 @@ export const settingsModule = angular
|
||||
)
|
||||
.component(
|
||||
'ldapSettingsTestLogin',
|
||||
r2a(withReactQuery(LdapSettingsTestLogin), [
|
||||
'settings',
|
||||
'limitedFeatureId',
|
||||
'showBeIndicatorIfNeeded',
|
||||
'isLimitedFeatureSelfContained',
|
||||
])
|
||||
r2a(withReactQuery(LdapSettingsTestLogin), ['settings'])
|
||||
)
|
||||
.component(
|
||||
'ldapSecurityFieldset',
|
||||
@@ -106,7 +98,6 @@ export const settingsModule = angular
|
||||
'onChange',
|
||||
'errors',
|
||||
'uploadState',
|
||||
'limitedFeatureId',
|
||||
'title',
|
||||
])
|
||||
)
|
||||
@@ -127,8 +118,6 @@ export const settingsModule = angular
|
||||
'onAutoPopulateChange',
|
||||
'selectedAdminGroups',
|
||||
'onSelectedAdminGroupsChange',
|
||||
'limitedFeatureId',
|
||||
'isLimitedFeatureSelfContained',
|
||||
]
|
||||
)
|
||||
).name;
|
||||
|
||||
@@ -13,7 +13,6 @@ export const switchField = r2a(SwitchField, [
|
||||
'data-cy',
|
||||
'disabled',
|
||||
'onChange',
|
||||
'featureId',
|
||||
'switchClass',
|
||||
'setTooltipHtmlMessage',
|
||||
'valueExplanation',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { r2a } from '@/react-tools/react2angular';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { ListView } from '@/react/portainer/environments/ListView';
|
||||
import { EdgeAutoCreateScriptViewWrapper } from '@/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView';
|
||||
import { ItemView } from '@/react/portainer/environments/ItemView/ItemView';
|
||||
|
||||
export const environmentsModule = angular
|
||||
@@ -16,8 +15,4 @@ export const environmentsModule = angular
|
||||
.component(
|
||||
'environmentsItemView',
|
||||
r2a(withUIRouter(withCurrentUser(ItemView)), [])
|
||||
)
|
||||
.component(
|
||||
'edgeAutoCreateScriptView',
|
||||
r2a(withUIRouter(withCurrentUser(EdgeAutoCreateScriptViewWrapper)), [])
|
||||
).name;
|
||||
|
||||
@@ -13,7 +13,6 @@ import { CreateHelmRepositoriesView } from '@/react/portainer/account/helm-repos
|
||||
|
||||
import { wizardModule } from './wizard';
|
||||
import { teamsModule } from './teams';
|
||||
import { updateSchedulesModule } from './update-schedules';
|
||||
import { environmentGroupModule } from './env-groups';
|
||||
import { registriesModule } from './registries';
|
||||
import { activityLogsModule } from './activity-logs';
|
||||
@@ -26,7 +25,6 @@ export const viewsModule = angular
|
||||
.module('portainer.app.react.views', [
|
||||
wizardModule,
|
||||
teamsModule,
|
||||
updateSchedulesModule,
|
||||
environmentGroupModule,
|
||||
registriesModule,
|
||||
activityLogsModule,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import { StateRegistry } from '@uirouter/angularjs';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import {
|
||||
ListView,
|
||||
CreateView,
|
||||
ItemView,
|
||||
} from '@/react/portainer/environments/update-schedules';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
|
||||
export const updateSchedulesModule = angular
|
||||
.module('portainer.edge.updateSchedules', [])
|
||||
.component(
|
||||
'updateSchedulesListView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), [])
|
||||
)
|
||||
.component(
|
||||
'updateSchedulesCreateView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(CreateView))), [])
|
||||
)
|
||||
.component(
|
||||
'updateSchedulesItemView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ItemView))), [])
|
||||
)
|
||||
.config(config).name;
|
||||
|
||||
function config($stateRegistryProvider: StateRegistry) {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.endpoints.updateSchedules',
|
||||
url: '/update-schedules',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'updateSchedulesListView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
docs: '/admin/environments/update',
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.endpoints.updateSchedules.create',
|
||||
url: '/update-schedules/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'updateSchedulesCreateView',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.endpoints.updateSchedules.item',
|
||||
url: '/update-schedules/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'updateSchedulesItemView',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
export default class AdSettingsController {
|
||||
/* @ngInject */
|
||||
constructor(LDAPService, $scope) {
|
||||
this.LDAPService = LDAPService;
|
||||
this.$scope = $scope;
|
||||
|
||||
this.domainSuffix = '';
|
||||
this.limitedFeatureId = FeatureId.HIDE_INTERNAL_AUTH;
|
||||
this.onTlscaCertChange = this.onTlscaCertChange.bind(this);
|
||||
this.searchUsers = this.searchUsers.bind(this);
|
||||
this.searchGroups = this.searchGroups.bind(this);
|
||||
this.parseDomainName = this.parseDomainName.bind(this);
|
||||
this.onAccountChange = this.onAccountChange.bind(this);
|
||||
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
|
||||
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
|
||||
}
|
||||
|
||||
onAutoUserProvisionChange(value) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.settings.AutoCreateUsers = value;
|
||||
});
|
||||
}
|
||||
|
||||
parseDomainName(account) {
|
||||
this.domainName = '';
|
||||
|
||||
if (!account || !account.includes('@')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, domainName] = account.split('@');
|
||||
if (!domainName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = _.compact(domainName.split('.'));
|
||||
this.domainSuffix = parts.map((part) => `dc=${part}`).join(',');
|
||||
}
|
||||
|
||||
onAccountChange(account) {
|
||||
this.parseDomainName(account);
|
||||
}
|
||||
|
||||
searchUsers() {
|
||||
return this.LDAPService.users(this.settings);
|
||||
}
|
||||
|
||||
searchGroups() {
|
||||
return this.LDAPService.groups(this.settings);
|
||||
}
|
||||
|
||||
onTlscaCertChange(file) {
|
||||
this.tlscaCert = file;
|
||||
}
|
||||
|
||||
addLDAPUrl() {
|
||||
this.settings.URLs.push('');
|
||||
}
|
||||
|
||||
removeLDAPUrl(index) {
|
||||
this.settings.URLs.splice(index, 1);
|
||||
}
|
||||
|
||||
isSaveSettingButtonDisabled() {
|
||||
return isLimitedToBE(this.limitedFeatureId) || !this.isLdapFormValid();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.tlscaCert = this.settings.TLSCACert;
|
||||
this.parseDomainName(this.settings.ReaderDN);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
<ng-form class="ad-settings" limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-class="limited-be">
|
||||
<div class="be-indicator-container">
|
||||
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeatureId"></be-feature-indicator></div>
|
||||
<div class="limited-be-content">
|
||||
<auto-user-provision-toggle
|
||||
value="$ctrl.settings.AutoCreateUsers"
|
||||
on-change="($ctrl.onAutoUserProvisionChange)"
|
||||
description="'With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). If disabled, users must be created in Portainer beforehand.'"
|
||||
></auto-user-provision-toggle>
|
||||
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title"> Information </div>
|
||||
<div class="form-group col-sm-12 text-muted small">
|
||||
When using Microsoft AD authentication, Portainer will delegate user authentication to the Domain Controller(s) configured below; if there is no connectivity, Portainer
|
||||
will fallback to internal authentication.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> AD configuration </div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
You can configure multiple AD Controllers for authentication fallback. Make sure all servers are using the same configuration (i.e. if TLS is enabled, they should all
|
||||
use the same certificates).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_url" class="col-sm-3 col-lg-2 control-label text-left" style="display: flex; flex-wrap: wrap">
|
||||
AD Controller
|
||||
<button
|
||||
type="button"
|
||||
class="label label-default interactive vertical-center"
|
||||
style="border: 0"
|
||||
ng-click="$ctrl.addLDAPUrl()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'plus-circle'"></pr-icon>
|
||||
Add additional server
|
||||
</button>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<div ng-repeat="url in $ctrl.settings.URLs track by $index" style="display: flex; margin-bottom: 10px">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="ldap-url"
|
||||
class="form-control"
|
||||
id="ldap_url"
|
||||
ng-model="$ctrl.settings.URLs[$index]"
|
||||
placeholder="e.g. 10.0.0.10:389 or myldap.domain.tld:389"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
<button
|
||||
ng-if="$index > 0"
|
||||
class="btn btn-sm btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.removeLDAPUrl($index)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_username" class="col-sm-3 control-label text-left">
|
||||
Service Account
|
||||
<portainer-tooltip message="'Account that will be used to search for users.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="ldap-username"
|
||||
class="form-control"
|
||||
id="ldap_username"
|
||||
ng-model="$ctrl.settings.ReaderDN"
|
||||
placeholder="reader@domain.tld"
|
||||
ng-change="$ctrl.onAccountChange($ctrl.settings.ReaderDN)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_password" class="col-sm-3 control-label text-left">
|
||||
Service Account Password
|
||||
<portainer-tooltip message="'If you do not enter a password, Portainer will leave the current password unchanged.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="ldap_password"
|
||||
ng-model="$ctrl.settings.Password"
|
||||
placeholder="password"
|
||||
autocomplete="new-password"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ldap-connectivity-check
|
||||
ng-if="!$ctrl.settings.TLSConfig.TLS && !$ctrl.settings.StartTLS"
|
||||
settings="$ctrl.settings"
|
||||
state="$ctrl.state"
|
||||
connectivity-check="$ctrl.connectivityCheck"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-connectivity-check>
|
||||
|
||||
<ldap-settings-security
|
||||
title="AD Connectivity Security"
|
||||
settings="$ctrl.settings"
|
||||
tlsca-cert="$ctrl.tlscaCert"
|
||||
upload-in-progress="$ctrl.state.uploadInProgress"
|
||||
on-tlsca-cert-change="($ctrl.onTlscaCertChange)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-settings-security>
|
||||
|
||||
<ldap-connectivity-check
|
||||
ng-if="$ctrl.settings.TLSConfig.TLS || $ctrl.settings.StartTLS"
|
||||
settings="$ctrl.settings"
|
||||
state="$ctrl.state"
|
||||
connectivity-check="$ctrl.connectivityCheck"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-connectivity-check>
|
||||
|
||||
<ldap-user-search
|
||||
style="margin-top: 5px"
|
||||
show-username-format="true"
|
||||
settings="$ctrl.settings.SearchSettings"
|
||||
domain-suffix="{{ $ctrl.domainSuffix }}"
|
||||
base-filter="(objectClass=user)"
|
||||
on-search-click="($ctrl.searchUsers)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-user-search>
|
||||
|
||||
<ldap-group-search
|
||||
style="margin-top: 5px"
|
||||
settings="$ctrl.settings.GroupSearchSettings"
|
||||
domain-suffix="{{ $ctrl.domainSuffix }}"
|
||||
base-filter="(objectClass=group)"
|
||||
on-search-click="($ctrl.searchGroups)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-group-search>
|
||||
|
||||
<ldap-custom-admin-group
|
||||
style="margin-top: 5px"
|
||||
settings="$ctrl.settings"
|
||||
on-search-click="($ctrl.onSearchAdminGroupsClick)"
|
||||
selected-admin-groups="$ctrl.selectedAdminGroups"
|
||||
default-admin-group-search-filter="'(objectClass=groupOfNames)'"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
is-limited-feature-self-contained="false"
|
||||
></ldap-custom-admin-group>
|
||||
|
||||
<ldap-settings-test-login settings="$ctrl.settings" limited-feature-id="$ctrl.limitedFeatureId" is-limited-feature-self-contained="false"></ldap-settings-test-login>
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
save-button-disabled="($ctrl.isSaveSettingButtonDisabled())"
|
||||
></save-auth-settings-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -1,15 +0,0 @@
|
||||
import controller from './ad-settings.controller';
|
||||
|
||||
export const adSettings = {
|
||||
templateUrl: './ad-settings.html',
|
||||
controller,
|
||||
bindings: {
|
||||
settings: '=',
|
||||
tlscaCert: '=',
|
||||
state: '=',
|
||||
connectivityCheck: '<',
|
||||
onSaveSettings: '<',
|
||||
saveButtonState: '<',
|
||||
isLdapFormValid: '&?',
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { adSettings } from './ad-settings';
|
||||
import { ldapSettings } from './ldap-settings';
|
||||
import { ldapSettingsCustom } from './ldap-settings-custom';
|
||||
import { ldapSettingsOpenLdap } from './ldap-settings-openldap';
|
||||
|
||||
import { ldapConnectivityCheck } from './ldap-connectivity-check';
|
||||
import { ldapGroupSearch } from './ldap-group-search';
|
||||
@@ -22,13 +20,11 @@ export default angular
|
||||
.service('LDAP', LDAP)
|
||||
.component('ldapConnectivityCheck', ldapConnectivityCheck)
|
||||
.component('ldapSettings', ldapSettings)
|
||||
.component('adSettings', adSettings)
|
||||
.component('ldapGroupSearch', ldapGroupSearch)
|
||||
.component('ldapGroupSearchItem', ldapGroupSearchItem)
|
||||
.component('ldapUserSearch', ldapUserSearch)
|
||||
.component('ldapUserSearchItem', ldapUserSearchItem)
|
||||
.component('ldapSettingsCustom', ldapSettingsCustom)
|
||||
.component('ldapCustomGroupSearch', ldapCustomGroupSearch)
|
||||
.component('ldapSettingsOpenLdap', ldapSettingsOpenLdap)
|
||||
.component('ldapSettingsSecurity', ldapSettingsSecurity)
|
||||
.component('ldapCustomUserSearch', ldapCustomUserSearch).name;
|
||||
|
||||
@@ -4,6 +4,5 @@ export const ldapConnectivityCheck = {
|
||||
settings: '<',
|
||||
state: '<',
|
||||
connectivityCheck: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
ng-disabled="($ctrl.state.connectivityCheckInProgress) || (!$ctrl.settings.URLs.length) || ((!$ctrl.settings.ReaderDN || !$ctrl.settings.Password) && !$ctrl.settings.AnonymousMode)"
|
||||
ng-click="$ctrl.connectivityCheck()"
|
||||
button-spinner="$ctrl.state.connectivityCheckInProgress"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.connectivityCheckInProgress">Test connectivity</span>
|
||||
<span ng-show="$ctrl.state.connectivityCheckInProgress">Testing connectivity...</span>
|
||||
|
||||
@@ -6,6 +6,5 @@ export const ldapCustomGroupSearch = {
|
||||
bindings: {
|
||||
settings: '=',
|
||||
onSearchClick: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -50,17 +50,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 small" style="color: #ffa719">
|
||||
<pr-icon icon="'briefcase'" class-name="'icon icon-xs vertical-center'"></pr-icon>
|
||||
Users removal synchronize between groups and teams only available in
|
||||
<a href="https://www.portainer.io/features?from=custom-login-banner" target="_blank">business edition.</a>
|
||||
<portainer-tooltip
|
||||
class="text-muted align-bottom"
|
||||
message="'Groups allows users to automatically be added to Portainer teams. However, automatically removing users from teams to keep it fully in sync is available in the Business Edition.'"
|
||||
></portainer-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -71,15 +60,6 @@
|
||||
Add group search configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px">
|
||||
<be-teaser-button
|
||||
feature-id="$ctrl.limitedFeatureId"
|
||||
heading="'Display User/Group matching'"
|
||||
message="'Show the list of users and groups that match the Portainer search configurations.'"
|
||||
button-text="'Display User/Group matching'"
|
||||
button-class-name="'!ml-0'"
|
||||
></be-teaser-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ldap-groups-datatable ng-if="$ctrl.showTable" dataset="$ctrl.groups"></ldap-groups-datatable>
|
||||
|
||||
@@ -6,6 +6,5 @@ export const ldapCustomUserSearch = {
|
||||
bindings: {
|
||||
settings: '=',
|
||||
onSearchClick: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,15 +53,6 @@
|
||||
Add user search configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px">
|
||||
<be-teaser-button
|
||||
feature-id="$ctrl.limitedFeatureId"
|
||||
heading="'Display Users'"
|
||||
message="'Allows you to display users from your LDAP server.'"
|
||||
button-text="'Display Users'"
|
||||
button-class-name="'!ml-0'"
|
||||
></be-teaser-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ldap-users-datatable ng-if="$ctrl.showTable" dataset="$ctrl.users"></ldap-users-datatable>
|
||||
|
||||
@@ -10,6 +10,5 @@ export const ldapGroupSearchItem = {
|
||||
baseFilter: '@',
|
||||
|
||||
onRemoveClick: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
class="btn btn-sm btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.onRemoveClick($ctrl.index)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
</button>
|
||||
@@ -17,7 +15,6 @@
|
||||
suffix="$ctrl.domainSuffix"
|
||||
value="$ctrl.config.GroupBaseDN"
|
||||
on-change="($ctrl.onChangeBaseDN)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-settings-dn-builder>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -42,8 +39,6 @@
|
||||
data-cy="ldap-group-search-item-select"
|
||||
ng-model="entry.type"
|
||||
ng-change="$ctrl.onGroupsChange()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<option value="ou">OU Name</option>
|
||||
<option value="cn">Folder Name</option>
|
||||
@@ -54,8 +49,6 @@
|
||||
class="form-control"
|
||||
ng-model="entry.value"
|
||||
ng-change="$ctrl.onGroupsChange()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-3 text-right">
|
||||
@@ -63,8 +56,6 @@
|
||||
class="btn btn-md btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.removeGroup($index)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
|
||||
</button>
|
||||
|
||||
@@ -9,6 +9,5 @@ export const ldapGroupSearch = {
|
||||
baseFilter: '@',
|
||||
|
||||
onSearchClick: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,19 +8,18 @@
|
||||
index="$index"
|
||||
base-filter="{{ $ctrl.baseFilter }}"
|
||||
on-remove-click="($ctrl.onRemoveClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-group-search-item>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 10px">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-light vertical-center !ml-0" ng-click="$ctrl.onAddClick()" limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-tabindex="-1">
|
||||
<button class="btn btn-sm btn-light vertical-center !ml-0" ng-click="$ctrl.onAddClick()">
|
||||
<pr-icon icon="'plus'"></pr-icon>
|
||||
Add group search configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px">
|
||||
<button class="btn btm-sm btn-primary !ml-0" type="button" ng-click="$ctrl.search()" limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-tabindex="-1">
|
||||
<button class="btn btm-sm btn-primary !ml-0" type="button" ng-click="$ctrl.search()">
|
||||
Display User/Group matching
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
export default class LdapSettingsCustomController {
|
||||
/* @ngInject */
|
||||
constructor($scope) {
|
||||
this.$scope = $scope;
|
||||
this.limitedFeatureId = FeatureId.EXTERNAL_AUTH_LDAP;
|
||||
|
||||
this.onAdminGroupSearchSettingsChange = this.onAdminGroupSearchSettingsChange.bind(this);
|
||||
this.onAutoPopulateChange = this.onAutoPopulateChange.bind(this);
|
||||
|
||||
@@ -35,15 +35,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<be-teaser-button
|
||||
feature-id="$ctrl.limitedFeatureId"
|
||||
heading="'Add additional server'"
|
||||
message="'Allows you to add an additional LDAP server.'"
|
||||
button-text="'Add additional server'"
|
||||
button-class-name="'!ml-0'"
|
||||
></be-teaser-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -57,8 +48,6 @@
|
||||
type="checkbox"
|
||||
id="anonymous_mode"
|
||||
ng-model="$ctrl.settings.AnonymousMode"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
data-cy="anonymous-mode-checkbox"
|
||||
/>
|
||||
<span class="slider round"></span>
|
||||
@@ -115,14 +104,12 @@
|
||||
class="block"
|
||||
settings="$ctrl.settings.SearchSettings"
|
||||
on-search-click="($ctrl.onSearchUsersClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-custom-user-search>
|
||||
|
||||
<ldap-custom-group-search
|
||||
class="block"
|
||||
settings="$ctrl.settings.GroupSearchSettings"
|
||||
on-search-click="($ctrl.onSearchGroupsClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-custom-group-search>
|
||||
|
||||
<ldap-custom-admin-group
|
||||
@@ -134,22 +121,13 @@
|
||||
on-auto-populate-change="($ctrl.onAutoPopulateChange)"
|
||||
selected-admin-groups="$ctrl.selectedAdminGroups"
|
||||
on-selected-admin-groups-change="($ctrl.onSelectedAdminGroupsChange)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
is-limited-feature-self-contained="true"
|
||||
></ldap-custom-admin-group>
|
||||
|
||||
<ldap-settings-test-login
|
||||
class="block"
|
||||
settings="$ctrl.settings"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
show-be-indicator-if-needed="true"
|
||||
is-limited-feature-self-contained="true"
|
||||
></ldap-settings-test-login>
|
||||
<ldap-settings-test-login class="block" settings="$ctrl.settings"></ldap-settings-test-login>
|
||||
</div>
|
||||
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="!$ctrl.saveButtonDisabled()"
|
||||
limited-feature-dir="{{ $ctrl.limitedFeatureId }}"
|
||||
></save-auth-settings-button>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import controller from './ldap-settings-openldap.controller';
|
||||
|
||||
export const ldapSettingsOpenLdap = {
|
||||
templateUrl: './ldap-settings-openldap.html',
|
||||
controller,
|
||||
bindings: {
|
||||
settings: '=',
|
||||
tlscaCert: '=',
|
||||
state: '=',
|
||||
connectivityCheck: '<',
|
||||
onTlscaCertChange: '<',
|
||||
onSearchUsersClick: '<',
|
||||
onSearchGroupsClick: '<',
|
||||
onSaveSettings: '<',
|
||||
saveButtonState: '<',
|
||||
saveButtonDisabled: '<',
|
||||
},
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
export default class LdapSettingsOpenLDAPController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
this.domainSuffix = '';
|
||||
this.limitedFeatureId = FeatureId.EXTERNAL_AUTH_LDAP;
|
||||
|
||||
this.findDomainSuffix = this.findDomainSuffix.bind(this);
|
||||
this.parseDomainSuffix = this.parseDomainSuffix.bind(this);
|
||||
this.onAccountChange = this.onAccountChange.bind(this);
|
||||
}
|
||||
|
||||
findDomainSuffix() {
|
||||
const serviceAccount = this.settings.ReaderDN;
|
||||
let domainSuffix = this.parseDomainSuffix(serviceAccount);
|
||||
if (!domainSuffix && this.settings.SearchSettings.length > 0) {
|
||||
const searchSettings = this.settings.SearchSettings[0];
|
||||
domainSuffix = this.parseDomainSuffix(searchSettings.BaseDN);
|
||||
}
|
||||
|
||||
this.domainSuffix = domainSuffix;
|
||||
}
|
||||
|
||||
parseDomainSuffix(string = '') {
|
||||
const index = string.toLowerCase().indexOf('dc=');
|
||||
return index !== -1 ? string.substring(index) : '';
|
||||
}
|
||||
|
||||
onAccountChange(serviceAccount) {
|
||||
this.domainSuffix = this.parseDomainSuffix(serviceAccount);
|
||||
}
|
||||
|
||||
addLDAPUrl() {
|
||||
this.settings.URLs.push('');
|
||||
}
|
||||
|
||||
removeLDAPUrl(index) {
|
||||
this.settings.URLs.splice(index, 1);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.findDomainSuffix();
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
<ng-form limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-class="limited-be" class="ldap-settings-openldap">
|
||||
<div class="be-indicator-container">
|
||||
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeatureId"></be-feature-indicator></div>
|
||||
<div class="limited-be-content">
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title"> Information </div>
|
||||
<div class="form-group col-sm-12 text-muted small">
|
||||
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> LDAP configuration </div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
You can configure multiple LDAP Servers for authentication fallback. Make sure all servers are using the same configuration (i.e. if TLS is enabled, they should all use
|
||||
the same certificates).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_url" class="col-sm-3 col-lg-2 control-label text-left" style="display: flex; flex-wrap: wrap">
|
||||
LDAP Server
|
||||
<button
|
||||
type="button"
|
||||
class="label label-default interactive vertical-center"
|
||||
style="border: 0"
|
||||
ng-click="$ctrl.addLDAPUrl()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'plus-circle'"></pr-icon>
|
||||
Add additional server
|
||||
</button>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<div ng-repeat="url in $ctrl.settings.URLs track by $index" style="display: flex; margin-bottom: 10px">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="ldap-url"
|
||||
class="form-control"
|
||||
id="ldap_url"
|
||||
ng-model="$ctrl.settings.URLs[$index]"
|
||||
placeholder="e.g. 10.0.0.10:389 or myldap.domain.tld:389"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
<button
|
||||
ng-if="$index > 0"
|
||||
class="btn btn-sm btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.removeLDAPUrl($index)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anonymous mode-->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="anonymous_mode" class="control-label col-sm-3 col-lg-2 text-left">
|
||||
Anonymous mode
|
||||
<portainer-tooltip message="'Enable this option if the server is configured for Anonymous access.'"></portainer-tooltip>
|
||||
</label>
|
||||
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="anonymous_mode"
|
||||
ng-model="$ctrl.settings.AnonymousMode"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
data-cy="ldap-anonymous-mode"
|
||||
/>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Anonymous mode-->
|
||||
|
||||
<div ng-if="!$ctrl.settings.AnonymousMode">
|
||||
<div class="form-group">
|
||||
<label for="ldap_username" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Reader DN
|
||||
<portainer-tooltip message="'Account that will be used to search for users.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="ldap-reader-dn"
|
||||
class="form-control"
|
||||
id="ldap_username"
|
||||
ng-model="$ctrl.settings.ReaderDN"
|
||||
placeholder="cn=user,dc=domain,dc=tld"
|
||||
ng-change="$ctrl.onAccountChange($ctrl.settings.ReaderDN)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_password" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Password
|
||||
<portainer-tooltip message="'If you do not enter a password, Portainer will leave the current password unchanged.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="ldap_password"
|
||||
ng-model="$ctrl.settings.Password"
|
||||
placeholder="password"
|
||||
autocomplete="new-password"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.settings.AnonymousMode">
|
||||
<label for="ldap_domain_root" class="col-sm-3 col-lg-2 control-label text-left"> Domain root </label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="ldap-domain-root"
|
||||
class="form-control"
|
||||
id="ldap_domain_root"
|
||||
ng-model="$ctrl.domainSuffix"
|
||||
placeholder="dc=domain,dc=tld"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ldap-connectivity-check
|
||||
ng-if="!$ctrl.settings.TLSConfig.TLS && !$ctrl.settings.StartTLS"
|
||||
settings="$ctrl.settings"
|
||||
state="$ctrl.state"
|
||||
connectivity-check="$ctrl.connectivityCheck"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-connectivity-check>
|
||||
|
||||
<ldap-settings-security
|
||||
title="Connectivity Security"
|
||||
settings="$ctrl.settings"
|
||||
tlsca-cert="$ctrl.tlscaCert"
|
||||
upload-in-progress="$ctrl.state.uploadInProgress"
|
||||
on-tlsca-cert-change="($ctrl.onTlscaCertChange)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-settings-security>
|
||||
|
||||
<ldap-connectivity-check
|
||||
ng-if="$ctrl.settings.TLSConfig.TLS || $ctrl.settings.StartTLS"
|
||||
settings="$ctrl.settings"
|
||||
state="$ctrl.state"
|
||||
connectivity-check="$ctrl.connectivityCheck"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-connectivity-check>
|
||||
|
||||
<ldap-user-search
|
||||
style="margin-top: 5px"
|
||||
settings="$ctrl.settings.SearchSettings"
|
||||
domain-suffix="{{ $ctrl.domainSuffix }}"
|
||||
base-filter="(objectClass=inetOrgPerson)"
|
||||
on-search-click="($ctrl.onSearchUsersClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-user-search>
|
||||
|
||||
<ldap-group-search
|
||||
style="margin-top: 5px"
|
||||
settings="$ctrl.settings.GroupSearchSettings"
|
||||
domain-suffix="{{ $ctrl.domainSuffix }}"
|
||||
base-filter="(objectClass=groupOfNames)"
|
||||
on-search-click="($ctrl.onSearchGroupsClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-group-search>
|
||||
|
||||
<ldap-settings-test-login settings="$ctrl.settings" limited-feature-id="$ctrl.limitedFeatureId"></ldap-settings-test-login>
|
||||
<save-auth-settings-button
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="!$ctrl.saveButtonDisabled()"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></save-auth-settings-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-form>
|
||||
@@ -6,7 +6,6 @@ export const ldapSettingsSecurity = {
|
||||
onTlscaCertChange: '<',
|
||||
uploadInProgress: '<',
|
||||
title: '@',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
controller: LdapController,
|
||||
};
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
on-change="($ctrl.onChangeReactValues)"
|
||||
upload-state="$ctrl.getUploadState()"
|
||||
title="$ctrl.title"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-security-fieldset>
|
||||
|
||||
@@ -46,16 +46,3 @@ export function buildAdSettingsModel() {
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function buildOpenLDAPSettingsModel() {
|
||||
const settings = buildLdapSettingsModel();
|
||||
|
||||
settings.ServerType = 1;
|
||||
settings.AnonymousMode = false;
|
||||
settings.SearchSettings[0].UserNameAttribute = 'uid';
|
||||
settings.SearchSettings[0].Filter = '(objectClass=inetOrgPerson)';
|
||||
settings.GroupSearchSettings[0].GroupAttribute = 'member';
|
||||
settings.GroupSearchSettings[0].GroupFilter = '(objectClass=groupOfNames)';
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import { buildLdapSettingsModel, buildOpenLDAPSettingsModel } from '@/portainer/settings/authentication/ldap/ldap-settings.model';
|
||||
import { options } from '@/react/portainer/settings/AuthenticationView/ldap-options';
|
||||
|
||||
const SERVER_TYPES = {
|
||||
CUSTOM: 0,
|
||||
OPEN_LDAP: 1,
|
||||
AD: 2,
|
||||
};
|
||||
|
||||
const DEFAULT_GROUP_FILTER = '(objectClass=groupOfNames)';
|
||||
const DEFAULT_USER_FILTER = '(objectClass=inetOrgPerson)';
|
||||
|
||||
export default class LdapSettingsController {
|
||||
/* @ngInject */
|
||||
constructor(LDAPService, $scope) {
|
||||
Object.assign(this, { LDAPService, SERVER_TYPES, $scope });
|
||||
Object.assign(this, { LDAPService, $scope });
|
||||
|
||||
this.tlscaCert = null;
|
||||
this.settingsDrafts = {};
|
||||
|
||||
this.boxSelectorOptions = options;
|
||||
|
||||
this.onTlscaCertChange = this.onTlscaCertChange.bind(this);
|
||||
this.searchUsers = this.searchUsers.bind(this);
|
||||
this.searchGroups = this.searchGroups.bind(this);
|
||||
this.onChangeServerType = this.onChangeServerType.bind(this);
|
||||
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
|
||||
this.onAutoUserProvisionChange = this.onAutoUserProvisionChange.bind(this);
|
||||
}
|
||||
|
||||
@@ -41,24 +27,6 @@ export default class LdapSettingsController {
|
||||
this.tlscaCert = this.settings.TLSConfig.TLSCACert;
|
||||
}
|
||||
|
||||
onChangeServerType(serverType) {
|
||||
this.settingsDrafts[this.settings.ServerType] = this.settings;
|
||||
|
||||
if (this.settingsDrafts[serverType]) {
|
||||
this.settings = this.settingsDrafts[serverType];
|
||||
return;
|
||||
}
|
||||
|
||||
switch (serverType) {
|
||||
case SERVER_TYPES.OPEN_LDAP:
|
||||
this.settings = buildOpenLDAPSettingsModel();
|
||||
break;
|
||||
case SERVER_TYPES.CUSTOM:
|
||||
this.settings = buildLdapSettingsModel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
searchUsers() {
|
||||
const settings = {
|
||||
...this.settings,
|
||||
|
||||
@@ -5,18 +5,7 @@
|
||||
description="'With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s). If disabled, users must be created in Portainer beforehand.'"
|
||||
></auto-user-provision-toggle>
|
||||
|
||||
<box-selector
|
||||
style="margin-bottom: 0"
|
||||
radio-name="'ldap-server-type-selector'"
|
||||
value="$ctrl.settings.ServerType"
|
||||
options="$ctrl.boxSelectorOptions"
|
||||
on-change="($ctrl.onChangeServerType)"
|
||||
slim="true"
|
||||
label="'Server Type'"
|
||||
></box-selector>
|
||||
|
||||
<ldap-settings-custom
|
||||
ng-if="$ctrl.settings.ServerType === $ctrl.SERVER_TYPES.CUSTOM"
|
||||
settings="$ctrl.settings"
|
||||
tlsca-cert="$ctrl.tlscaCert"
|
||||
state="$ctrl.state"
|
||||
@@ -28,17 +17,4 @@
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="$ctrl.isLdapFormValid"
|
||||
></ldap-settings-custom>
|
||||
<ldap-settings-open-ldap
|
||||
ng-if="$ctrl.settings.ServerType === $ctrl.SERVER_TYPES.OPEN_LDAP"
|
||||
settings="$ctrl.settings"
|
||||
tlsca-cert="$ctrl.tlscaCert"
|
||||
state="$ctrl.state"
|
||||
on-tlsca-cert-change="($ctrl.onTlscaCertChange)"
|
||||
connectivity-check="$ctrl.connectivityCheck"
|
||||
on-search-users-click="($ctrl.searchUsers)"
|
||||
on-search-groups-click="($ctrl.searchGroups)"
|
||||
on-save-settings="($ctrl.onSaveSettings)"
|
||||
save-button-state="($ctrl.saveButtonState)"
|
||||
save-button-disabled="$ctrl.isLdapFormValid"
|
||||
></ldap-settings-open-ldap>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,5 @@ export const ldapUserSearchItem = {
|
||||
domainSuffix: '@',
|
||||
baseFilter: '@',
|
||||
onRemoveClick: '<',
|
||||
limitedFeatureId: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
class="btn btn-sm btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.onRemoveClick($ctrl.index)"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
</button>
|
||||
@@ -25,16 +23,12 @@
|
||||
ng-model="$ctrl.config.UserNameAttribute"
|
||||
uib-btn-radio="'sAMAccountName'"
|
||||
style="margin-left: 0px"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>username</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
ng-model="$ctrl.config.UserNameAttribute"
|
||||
uib-btn-radio="'userPrincipalName'"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>user@domainname</button
|
||||
>
|
||||
</div>
|
||||
@@ -54,7 +48,6 @@
|
||||
label="'User Search Path (optional)'"
|
||||
suffix="$ctrl.domainSuffix"
|
||||
on-change="($ctrl.onBaseDNChange)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-settings-dn-builder>
|
||||
|
||||
<div class="form-group no-margin-last-child">
|
||||
@@ -65,8 +58,6 @@
|
||||
class="label label-default interactive vertical-center"
|
||||
style="margin-left: 10px; border: 0"
|
||||
ng-click="$ctrl.addGroup()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'plus-circle'"></pr-icon>
|
||||
add another group
|
||||
@@ -82,7 +73,6 @@
|
||||
suffix="$ctrl.domainSuffix"
|
||||
on-change="($ctrl.onGroupChange)"
|
||||
on-remove-click="($ctrl.removeGroup)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-settings-group-dn-builder>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -8,7 +8,6 @@ export const ldapUserSearch = {
|
||||
domainSuffix: '@',
|
||||
showUsernameFormat: '<',
|
||||
baseFilter: '@',
|
||||
limitedFeatureId: '<',
|
||||
|
||||
onSearchClick: '<',
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
show-username-format="$ctrl.showUsernameFormat"
|
||||
base-filter="{{ $ctrl.baseFilter }}"
|
||||
on-remove-click="($ctrl.onRemoveClick)"
|
||||
limited-feature-id="$ctrl.limitedFeatureId"
|
||||
></ldap-user-search-item>
|
||||
</div>
|
||||
|
||||
@@ -19,15 +18,13 @@
|
||||
class="label label-default interactive vertical-center"
|
||||
style="border: 0"
|
||||
ng-click="$ctrl.onAddClick()"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-tabindex="-1"
|
||||
>
|
||||
<pr-icon icon="'plus-circle'"></pr-icon>
|
||||
Add user search configuration
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 10px">
|
||||
<button class="btn btm-sm btn-primary" type="button" ng-click="$ctrl.search()" limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-tabindex="-1">
|
||||
<button class="btn btm-sm btn-primary" type="button" ng-click="$ctrl.search()">
|
||||
Display Users
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,6 @@ export const saveAuthSettingsButton = {
|
||||
onSaveSettings: '<',
|
||||
saveButtonDisabled: '<',
|
||||
saveButtonState: '<',
|
||||
limitedFeatureId: '<',
|
||||
limitedFeatureClass: '<',
|
||||
className: '<',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
ng-click="$ctrl.onSaveSettings()"
|
||||
ng-disabled="$ctrl.saveButtonDisabled || $ctrl.saveButtonState"
|
||||
button-spinner="$ctrl.saveButtonState"
|
||||
limited-feature-dir="{{::$ctrl.limitedFeatureId}}"
|
||||
limited-feature-class="{{::$ctrl.limitedFeatureClass}}"
|
||||
>
|
||||
<span ng-hide="$ctrl.saveButtonState">Save settings</span>
|
||||
<span ng-show="$ctrl.saveButtonState">Saving...</span>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
export default class AuthLogsViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, Notifications) {
|
||||
this.$async = $async;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.limitedFeature = FeatureId.ACTIVITY_AUDIT;
|
||||
this.state = {
|
||||
keyword: '',
|
||||
date: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
sort: {
|
||||
key: 'Timestamp',
|
||||
desc: true,
|
||||
},
|
||||
contextFilter: [1, 2, 3],
|
||||
typeFilter: [1, 2, 3],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalItems: 0,
|
||||
logs: null,
|
||||
};
|
||||
|
||||
this.today = moment().endOf('day');
|
||||
this.minValidDate = moment().subtract(7, 'd').startOf('day');
|
||||
|
||||
this.onChangeDate = this.onChangeDate.bind(this);
|
||||
this.onChangeKeyword = this.onChangeKeyword.bind(this);
|
||||
this.onChangeSort = this.onChangeSort.bind(this);
|
||||
this.onChangeContextFilter = this.onChangeContextFilter.bind(this);
|
||||
this.onChangeTypeFilter = this.onChangeTypeFilter.bind(this);
|
||||
this.loadLogs = this.loadLogs.bind(this);
|
||||
this.onChangePage = this.onChangePage.bind(this);
|
||||
this.onChangeLimit = this.onChangeLimit.bind(this);
|
||||
}
|
||||
|
||||
onChangePage(page) {
|
||||
this.state.page = page;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeLimit(limit) {
|
||||
this.state.page = 1;
|
||||
this.state.limit = limit;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeSort(sort) {
|
||||
this.state.page = 1;
|
||||
this.state.sort = sort;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeContextFilter(filterKey, filterState) {
|
||||
this.state.contextFilter = filterState;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeTypeFilter(filterKey, filterState) {
|
||||
this.state.typeFilter = filterState;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeKeyword(keyword) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.state.page = 1;
|
||||
this.state.keyword = keyword;
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
onChangeDate({ startDate, endDate }) {
|
||||
this.state.page = 1;
|
||||
this.state.date = { to: endDate, from: startDate };
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
async loadLogs() {
|
||||
return this.$async(async () => {
|
||||
this.state.logs = null;
|
||||
try {
|
||||
const { logs, totalCount } = { logs: [], totalCount: 0 };
|
||||
this.state.logs = decorateLogs(logs);
|
||||
this.state.totalItems = totalCount;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed loading auth activity logs');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function decorateLogs(logs) {
|
||||
return logs;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<page-header title="'User authentication logs'" breadcrumbs="['User authentication logs']" reload="true"> </page-header>
|
||||
|
||||
<div class="mx-4">
|
||||
<div class="be-indicator-container limited-be">
|
||||
<div class="limited-be-link vertical-center m-4"><be-feature-indicator feature="$ctrl.limitedFeature"></be-feature-indicator></div>
|
||||
<!-- 15px matches the padding for col-sm-12 for the widget and table -->
|
||||
<div class="limited-be-content !p-0 !pt-[15px]">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="dateRangeInput" class="col-sm-2 control-label text-left">Date range</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" class-name="'icon icon-sm icon-primary'"></pr-icon>
|
||||
Portainer user authentication logs have a maximum retention of 7 days.
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-primary" limited-feature-dir="{{::$ctrl.limitedFeature}}" limited-feature-class="limited-be" limited-feature-disabled
|
||||
><pr-icon icon="'download'" class-name="'icon icon-sm'"></pr-icon>Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<authentication-logs-table
|
||||
dataset="$ctrl.state.logs"
|
||||
keyword="$ctrl.state.keyword"
|
||||
sort="$ctrl.state.sort"
|
||||
limit="$ctrl.state.limit"
|
||||
context-filter="$ctrl.state.contextFilter"
|
||||
type-filter="$ctrl.state.typeFilter"
|
||||
total-items="$ctrl.state.totalItems"
|
||||
current-page="$ctrl.state.currentPage"
|
||||
feature="{{:: $ctrl.limitedFeature}}"
|
||||
on-change-context-filter="($ctrl.onChangeContextFilter)"
|
||||
on-change-type-filter="($ctrl.onChangeTypeFilter)"
|
||||
on-change-keyword="($ctrl.onChangeKeyword)"
|
||||
on-change-sort="($ctrl.onChangeSort)"
|
||||
on-change-limit="($ctrl.onChangeLimit)"
|
||||
on-change-page="($ctrl.onChangePage)"
|
||||
></authentication-logs-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
import controller from './auth-logs-view.controller.js';
|
||||
|
||||
export const authLogsView = {
|
||||
templateUrl: './auth-logs-view.html',
|
||||
controller,
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { authLogsView } from './auth-logs-view';
|
||||
|
||||
export default angular.module('portainer.app.user-activity.auth-logs-view', []).component('authLogsView', authLogsView).name;
|
||||
@@ -2,33 +2,14 @@ import angular from 'angular';
|
||||
|
||||
import { NotificationsViewAngular } from '@/react/portainer/notifications/NotificationsView';
|
||||
import { AccessHeaders } from '../authorization-guard';
|
||||
import authLogsViewModule from './auth-logs-view';
|
||||
import { UserActivityService } from './user-activity.service';
|
||||
import { UserActivity } from './user-activity.rest';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.user-activity', [authLogsViewModule])
|
||||
.service('UserActivity', UserActivity)
|
||||
.service('UserActivityService', UserActivityService)
|
||||
.module('portainer.app.user-activity', [])
|
||||
.component('notifications', NotificationsViewAngular)
|
||||
.config(config).name;
|
||||
|
||||
/* @ngInject */
|
||||
function config($stateRegistryProvider) {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.authLogs',
|
||||
url: '/auth-logs',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'authLogsView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
docs: '/admin/logs',
|
||||
access: AccessHeaders.Admin,
|
||||
},
|
||||
});
|
||||
|
||||
$stateRegistryProvider.register({
|
||||
name: 'portainer.activityLogs',
|
||||
url: '/activity-logs',
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
/* @ngInject */
|
||||
export function UserActivity($resource, $http) {
|
||||
const BASE_URL = baseHref() + 'api/useractivity';
|
||||
|
||||
const resource = $resource(
|
||||
`${BASE_URL}/:action`,
|
||||
{},
|
||||
{
|
||||
authLogs: { method: 'GET', params: { action: 'authlogs' } },
|
||||
}
|
||||
);
|
||||
|
||||
return { authLogsAsCSV, ...resource };
|
||||
|
||||
async function authLogsAsCSV(params) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: `${BASE_URL}/authlogs.csv`,
|
||||
params,
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-type': 'text/csv',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function UserActivityService(FileSaver, UserActivity) {
|
||||
return { authLogs, saveAuthLogsAsCSV };
|
||||
|
||||
function authLogs(offset, limit, sort, keyword, date, contexts, types) {
|
||||
return UserActivity.authLogs({ offset, limit, keyword, before: date.to, after: date.from, sortBy: sort.key, sortDesc: sort.desc, contexts, types }).$promise;
|
||||
}
|
||||
|
||||
async function saveAuthLogsAsCSV(sort, keyword, date, contexts, types) {
|
||||
const response = await UserActivity.authLogsAsCSV({ keyword, before: date.to, after: date.from, sortBy: sort.key, sortDesc: sort.desc, limit: 2000, contexts, types });
|
||||
return FileSaver.saveAs(response.data, 'logs.csv');
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,5 @@
|
||||
entity-type="endpoint"
|
||||
inherit-from="ctrl.group"
|
||||
update-access="ctrl.updateAccess"
|
||||
limited-feature="ctrl.limitedFeature"
|
||||
>
|
||||
</por-access-management>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
class EndpointAccessController {
|
||||
/* @ngInject */
|
||||
constructor($state, $transition$, Notifications, EndpointService, GroupService, $async) {
|
||||
@@ -12,8 +10,6 @@ class EndpointAccessController {
|
||||
this.GroupService = GroupService;
|
||||
this.$async = $async;
|
||||
|
||||
this.limitedFeature = FeatureId.RBAC_ROLES;
|
||||
|
||||
this.updateAccess = this.updateAccess.bind(this);
|
||||
this.updateAccessAsync = this.updateAccessAsync.bind(this);
|
||||
}
|
||||
|
||||
@@ -137,148 +137,46 @@
|
||||
</div>
|
||||
<!-- !note -->
|
||||
|
||||
<box-selector slim="true" options="restoreOptions" value="formValues.restoreFormType" on-change="(onChangeRestoreType)" radio-name="'restore-type'"></box-selector>
|
||||
|
||||
<div ng-if="formValues.restoreFormType === RESTORE_FORM_TYPES.FILE">
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted"> You can upload a backup file from your computer. </span>
|
||||
</div>
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted"> You can upload a backup file from your computer. </span>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<!-- select-file-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 vertical-center">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select
|
||||
accept=".gz,.encrypted"
|
||||
ngf-accept="'application/x-tar,application/x-gzip'"
|
||||
ng-model="formValues.BackupFile"
|
||||
auto-focus
|
||||
data-cy="init-selectBackupFileButton"
|
||||
>Select file</button
|
||||
>
|
||||
<span class="space-left vertical-center">
|
||||
{{ formValues.BackupFile.name }}
|
||||
<pr-icon icon="'x-circle'" class-name="'icon-danger'" ng-if="!formValues.BackupFile"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- password-input -->
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label text-left">
|
||||
Password
|
||||
<portainer-tooltip
|
||||
message="'If the backup is password protected, provide the password in order to extract the backup file, otherwise this field can be left empty.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="init-backupPasswordInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !password-input -->
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<!-- select-file-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 vertical-center">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select
|
||||
accept=".gz,.encrypted"
|
||||
ngf-accept="'application/x-tar,application/x-gzip'"
|
||||
ng-model="formValues.BackupFile"
|
||||
auto-focus
|
||||
data-cy="init-selectBackupFileButton"
|
||||
>Select file</button
|
||||
>
|
||||
<span class="space-left vertical-center">
|
||||
{{ formValues.BackupFile.name }}
|
||||
<pr-icon icon="'x-circle'" class-name="'icon-danger'" ng-if="!formValues.BackupFile"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- password-input -->
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label text-left">
|
||||
Password
|
||||
<portainer-tooltip
|
||||
message="'If the backup is password protected, provide the password in order to extract the backup file, otherwise this field can be left empty.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="init-backupPasswordInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !password-input -->
|
||||
<!-- !select-file-input -->
|
||||
<div class="limited-be-content" ng-if="formValues.restoreFormType === RESTORE_FORM_TYPES.S3">
|
||||
<!-- Access key id -->
|
||||
<div class="form-group">
|
||||
<label for="access_key_id" class="col-sm-3 control-label text-left">Access key ID</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="access_key_id" name="access_key_id" ng-model="formValues.AccessKeyId" required data-cy="init-accessKeyIdInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Access key id -->
|
||||
<!-- Secret access key -->
|
||||
<div class="form-group">
|
||||
<label for="secret_access_key" class="col-sm-3 control-label text-left">Secret access key</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="password"
|
||||
data-cy="init-secretAccessKeyInput"
|
||||
class="form-control"
|
||||
id="secret_access_key"
|
||||
name="secret_access_key"
|
||||
ng-model="formValues.SecretAccessKey"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Secret access key -->
|
||||
<!-- Region -->
|
||||
<div class="form-group">
|
||||
<label for="backup-s3-region" class="col-sm-3 control-label text-left">Region</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="init-s3RegionInput"
|
||||
class="form-control"
|
||||
placeholder="default region is us-east-1 if left empty"
|
||||
id="backup-s3-region"
|
||||
name="backup-s3-region"
|
||||
ng-model="formValues.Region"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Region -->
|
||||
<!-- Bucket name -->
|
||||
<div class="form-group">
|
||||
<label for="bucket_name" class="col-sm-3 control-label text-left">Bucket name</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="bucket_name" name="bucket_name" ng-model="formValues.BucketName" required data-cy="init-bucketNameInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Bucket name -->
|
||||
<!-- S3 Compatible Host -->
|
||||
<div class="form-group">
|
||||
<label for="s3-compatible-host" class="col-sm-3 control-label text-left">
|
||||
S3 Compatible Host
|
||||
<portainer-tooltip message="'Hostname of a S3 service'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="init-s3CompatibleHostInput"
|
||||
class="form-control"
|
||||
id="s3-compatible-host"
|
||||
name="s3-compatible-host"
|
||||
ng-model="formValues.S3CompatibleHost"
|
||||
placeholder="leave empty for AWS S3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !S3 Compatible Host -->
|
||||
<!-- Filename -->
|
||||
<div class="form-group">
|
||||
<label for="backup-s3-filename" class="col-sm-3 control-label text-left">Filename</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="init-backupFilenameInput"
|
||||
class="form-control"
|
||||
id="backup-s3-filename"
|
||||
name="backup-s3-filename"
|
||||
ng-model="formValues.Filename"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Filename -->
|
||||
<!-- password-input -->
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label text-left">
|
||||
Password
|
||||
<portainer-tooltip
|
||||
message="'If the backup is password protected, provide the password in order to extract the backup file, otherwise this field can be left empty.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="init-backupPasswordInput" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !password-input -->
|
||||
</div>
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getEnvironments } from '@/react/portainer/environments/environment.service';
|
||||
import { restoreOptions } from '@/react/portainer/init/InitAdminView/restore-options';
|
||||
|
||||
const REDIRECT_REASON_TIMEOUT = 'AdminInitTimeout';
|
||||
|
||||
@@ -14,19 +13,15 @@ angular.module('portainer.app').controller('InitAdminController', [
|
||||
'BackupService',
|
||||
'StatusService',
|
||||
function ($scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, BackupService, StatusService) {
|
||||
$scope.restoreOptions = restoreOptions;
|
||||
|
||||
$scope.uploadBackup = uploadBackup;
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
$scope.RESTORE_FORM_TYPES = { S3: 's3', FILE: 'file' };
|
||||
|
||||
$scope.formValues = {
|
||||
Username: 'admin',
|
||||
Password: '',
|
||||
ConfirmPassword: '',
|
||||
SetupToken: '',
|
||||
restoreFormType: $scope.RESTORE_FORM_TYPES.FILE,
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
@@ -42,13 +37,6 @@ angular.module('portainer.app').controller('InitAdminController', [
|
||||
$scope.state.showRestorePortainer = !$scope.state.showRestorePortainer;
|
||||
};
|
||||
|
||||
$scope.onChangeRestoreType = onChangeRestoreType;
|
||||
function onChangeRestoreType(value) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.restoreFormType = value;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.createAdminUser = function () {
|
||||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
|
||||
@@ -38,17 +38,6 @@
|
||||
is-ldap-form-valid="isLDAPFormValid"
|
||||
></ldap-settings>
|
||||
|
||||
<ad-settings
|
||||
ng-if="authenticationMethodSelected(4)"
|
||||
settings="formValues.ldap.adSettings"
|
||||
tlsca-cert="formValues.TLSCACert"
|
||||
state="state"
|
||||
connectivity-check="LDAPConnectivityCheck"
|
||||
on-save-settings="(saveSettings)"
|
||||
save-button-state="state.actionInProgress"
|
||||
is-ldap-form-valid="isLDAPFormValid()"
|
||||
></ad-settings>
|
||||
|
||||
<oauth-settings
|
||||
ng-if="authenticationMethodSelected(3)"
|
||||
settings="OAuthSettings"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.be-indicator {
|
||||
@apply border border-solid border-gray-6;
|
||||
@apply text-xs text-gray-6;
|
||||
border-radius: 15px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 400;
|
||||
touch-action: all;
|
||||
pointer-events: all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.be-indicator .be-indicator-icon {
|
||||
@apply text-inherit;
|
||||
}
|
||||
|
||||
.be-indicator:hover {
|
||||
@apply underline;
|
||||
@apply border-blue-9 text-blue-9;
|
||||
}
|
||||
|
||||
.be-indicator:hover .be-indicator-label {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
.be-indicator-container {
|
||||
@apply relative;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Meta } from '@storybook/react-webpack5';
|
||||
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/react/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({});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Briefcase } from 'lucide-react';
|
||||
|
||||
import './BEFeatureIndicator.css';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { getFeatureDetails } from './utils';
|
||||
|
||||
export interface Props {
|
||||
featureId: FeatureId;
|
||||
showIcon?: boolean;
|
||||
className?: string;
|
||||
children?: (isLimited: boolean) => ReactNode;
|
||||
}
|
||||
|
||||
export function BEFeatureIndicator({
|
||||
featureId,
|
||||
children = () => null,
|
||||
showIcon = true,
|
||||
className = '',
|
||||
}: Props) {
|
||||
const { url, limitedToBE = false } = getFeatureDetails(featureId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{limitedToBE && (
|
||||
<a
|
||||
className={clsx('be-indicator vertical-center text-xs', className)}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{showIcon && (
|
||||
<Icon icon={Briefcase} className="be-indicator-icon mr-1" />
|
||||
)}
|
||||
<span className="be-indicator-label break-words">
|
||||
Business Feature
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{children(limitedToBE)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { BEFeatureIndicator } from './BEFeatureIndicator';
|
||||
|
||||
type Variants = 'form-section' | 'widget' | 'multi-widget';
|
||||
|
||||
type OverlayClasses = {
|
||||
beLinkContainerClassName: string;
|
||||
contentClassName: string;
|
||||
};
|
||||
|
||||
const variantClassNames: Record<Variants, OverlayClasses> = {
|
||||
'form-section': {
|
||||
beLinkContainerClassName: '',
|
||||
contentClassName: '',
|
||||
},
|
||||
widget: {
|
||||
beLinkContainerClassName: '',
|
||||
// no padding so that the border overlaps the widget border
|
||||
contentClassName: '!p-0',
|
||||
},
|
||||
'multi-widget': {
|
||||
beLinkContainerClassName: 'm-4',
|
||||
// widgets have a mx of 15px and mb of 15px - match this at the top with padding
|
||||
contentClassName: '!p-0 !pt-[15px]',
|
||||
},
|
||||
};
|
||||
|
||||
export function BEOverlay({
|
||||
featureId,
|
||||
children,
|
||||
variant = 'form-section',
|
||||
}: {
|
||||
featureId: FeatureId;
|
||||
children: React.ReactNode;
|
||||
variant?: 'form-section' | 'widget' | 'multi-widget';
|
||||
}) {
|
||||
const isLimited = isLimitedToBE(featureId);
|
||||
if (!isLimited) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="be-indicator-container limited-be">
|
||||
<div
|
||||
className={clsx(
|
||||
'limited-be-link vertical-center',
|
||||
variantClassNames[variant].beLinkContainerClassName
|
||||
)}
|
||||
>
|
||||
<BEFeatureIndicator featureId={featureId} />
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'limited-be-content',
|
||||
variantClassNames[variant].contentClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { BEFeatureIndicator } from './BEFeatureIndicator';
|
||||
@@ -1,15 +0,0 @@
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/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 };
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Briefcase } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||
|
||||
interface Props extends AutomationTestingProps {
|
||||
featureId: FeatureId;
|
||||
heading: string;
|
||||
message: string;
|
||||
buttonText: string;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
}
|
||||
|
||||
export function BETeaserButton({
|
||||
featureId,
|
||||
heading,
|
||||
message,
|
||||
buttonText,
|
||||
className,
|
||||
buttonClassName,
|
||||
'data-cy': dataCy,
|
||||
}: Props) {
|
||||
return (
|
||||
<TooltipWithChildren
|
||||
className={className}
|
||||
heading={heading}
|
||||
BEFeatureID={featureId}
|
||||
message={message}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
className={buttonClassName}
|
||||
icon={Briefcase}
|
||||
type="button"
|
||||
color="default"
|
||||
size="small"
|
||||
onClick={() => {}}
|
||||
disabled
|
||||
data-cy={dataCy}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipWithChildren>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import { useState } from 'react';
|
||||
import { Anchor, Briefcase } from 'lucide-react';
|
||||
|
||||
import Docker from '@/assets/ico/vendor/docker.svg?c';
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BoxSelector } from './BoxSelector';
|
||||
import { BoxSelectorOption } from './types';
|
||||
@@ -16,7 +14,7 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
export { Example, LimitedFeature };
|
||||
export { Example };
|
||||
|
||||
function Example() {
|
||||
const [value, setValue] = useState(3);
|
||||
@@ -51,41 +49,6 @@ function Example() {
|
||||
);
|
||||
}
|
||||
|
||||
function LimitedFeature() {
|
||||
initFeatureService(Edition.CE);
|
||||
const [value, setValue] = useState(3);
|
||||
const options: BoxSelectorOption<number>[] = [
|
||||
{
|
||||
description: 'description 1',
|
||||
icon: Anchor,
|
||||
iconType: 'badge',
|
||||
id: '1',
|
||||
value: 3,
|
||||
label: 'option 1',
|
||||
},
|
||||
{
|
||||
description: 'description 2',
|
||||
icon: Briefcase,
|
||||
iconType: 'badge',
|
||||
id: '2',
|
||||
value: 4,
|
||||
label: 'option 2',
|
||||
feature: FeatureId.ACTIVITY_AUDIT,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BoxSelector
|
||||
radioName="name"
|
||||
onChange={(value: number) => {
|
||||
setValue(value);
|
||||
}}
|
||||
value={value}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MultiSelect() {
|
||||
const [value, setValue] = useState([3]);
|
||||
const options: BoxSelectorOption<number>[] = [
|
||||
|
||||
@@ -61,5 +61,5 @@ test('should render with the initial value selected and call onChange when click
|
||||
expect(item2.checked).toBeFalsy();
|
||||
|
||||
fireEvent.click(item2);
|
||||
expect(onChange).toHaveBeenCalledWith(options[1].value, false);
|
||||
expect(onChange).toHaveBeenCalledWith(options[1].value);
|
||||
});
|
||||
|
||||
@@ -9,13 +9,13 @@ import { BoxSelectorOption, Value } from './types';
|
||||
interface IsMultiProps<T extends Value> {
|
||||
isMulti: true;
|
||||
value: T[];
|
||||
onChange(value: T[], limitedToBE: boolean): void;
|
||||
onChange(value: T[]): void;
|
||||
}
|
||||
|
||||
interface SingleProps<T extends Value> {
|
||||
isMulti?: never;
|
||||
value: T;
|
||||
onChange(value: T, limitedToBE: boolean): void;
|
||||
onChange(value: T): void;
|
||||
}
|
||||
|
||||
type Union<T extends Value> = IsMultiProps<T> | SingleProps<T>;
|
||||
@@ -87,12 +87,12 @@ export function BoxSelector<T extends Value>({
|
||||
</>
|
||||
);
|
||||
|
||||
function handleSelect(optionValue: T, limitedToBE: boolean) {
|
||||
function handleSelect(optionValue: T) {
|
||||
if (props.isMulti) {
|
||||
const newValue = isSelected(optionValue)
|
||||
? props.value.filter((v) => v !== optionValue)
|
||||
: [...props.value, optionValue];
|
||||
props.onChange(newValue, limitedToBE);
|
||||
props.onChange(newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export function BoxSelector<T extends Value>({
|
||||
return;
|
||||
}
|
||||
|
||||
props.onChange(optionValue, limitedToBE);
|
||||
props.onChange(optionValue);
|
||||
}
|
||||
|
||||
function isSelected(optionValue: T) {
|
||||
|
||||
@@ -37,13 +37,3 @@
|
||||
.content {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* used for BE teaser */
|
||||
.box-selector-item.limited.business label,
|
||||
.box-selector-item.limited.business input:checked + label {
|
||||
@apply border-gray-6 bg-gray-6 bg-opacity-10;
|
||||
@apply th-dark:border-gray-6 th-dark:bg-gray-6 th-dark:bg-opacity-10;
|
||||
@apply th-highcontrast:border-gray-6 th-highcontrast:bg-gray-6 th-highcontrast:bg-opacity-10;
|
||||
|
||||
filter: none;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Meta } from '@storybook/react-webpack5';
|
||||
import { ReactNode } from 'react';
|
||||
import { Briefcase } from 'lucide-react';
|
||||
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import Docker from '@/assets/ico/vendor/docker.svg?c';
|
||||
|
||||
import { IconProps } from '@@/Icon';
|
||||
@@ -28,7 +26,6 @@ interface ExampleProps {
|
||||
description?: string;
|
||||
icon?: IconProps['icon'];
|
||||
label?: string;
|
||||
feature?: FeatureId;
|
||||
}
|
||||
|
||||
function Template({
|
||||
@@ -36,7 +33,6 @@ function Template({
|
||||
description = 'description',
|
||||
icon,
|
||||
label = 'label',
|
||||
feature,
|
||||
}: ExampleProps) {
|
||||
const option: BoxSelectorOption<number> = {
|
||||
description,
|
||||
@@ -44,7 +40,6 @@ function Template({
|
||||
id: 'id',
|
||||
label,
|
||||
value: 1,
|
||||
feature,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,18 +64,6 @@ 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 />;
|
||||
}
|
||||
|
||||
function IconTemplate({
|
||||
icon,
|
||||
iconType,
|
||||
|
||||
@@ -5,11 +5,9 @@ import { Fragment } from 'react';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
||||
|
||||
import styles from './BoxSelectorItem.module.css';
|
||||
import { BoxSelectorOption, Value } from './types';
|
||||
import { LimitedToBeBoxSelectorIndicator } from './LimitedToBeBoxSelectorIndicator';
|
||||
import { BoxOption } from './BoxOption';
|
||||
import { LogoIcon } from './LogoIcon';
|
||||
|
||||
@@ -18,7 +16,7 @@ type Props<T extends Value> = {
|
||||
radioName: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
onSelect(value: T, limitedToBE: boolean): void;
|
||||
onSelect(value: T): void;
|
||||
isSelected(value: T): boolean;
|
||||
type?: 'radio' | 'checkbox';
|
||||
slim?: boolean;
|
||||
@@ -36,37 +34,22 @@ export function BoxSelectorItem<T extends Value>({
|
||||
slim = false,
|
||||
checkIcon = Check,
|
||||
}: Props<T>) {
|
||||
const { limitedToBE = false, url: featureUrl } = getFeatureDetails(
|
||||
option.feature
|
||||
);
|
||||
|
||||
const ContentBox = slim ? 'div' : Fragment;
|
||||
|
||||
return (
|
||||
<BoxOption
|
||||
className={clsx(styles.boxSelectorItem, {
|
||||
[styles.business]: limitedToBE,
|
||||
[styles.limited]: limitedToBE,
|
||||
})}
|
||||
className={styles.boxSelectorItem}
|
||||
radioName={radioName}
|
||||
option={option}
|
||||
isSelected={isSelected}
|
||||
disabled={isDisabled()}
|
||||
onSelect={(value) => onSelect(value, limitedToBE)}
|
||||
onSelect={onSelect}
|
||||
tooltip={tooltip}
|
||||
type={type}
|
||||
checkIcon={checkIcon}
|
||||
>
|
||||
{limitedToBE && (
|
||||
<LimitedToBeBoxSelectorIndicator
|
||||
url={featureUrl}
|
||||
// show tooltip only for radio type options because be-only checkbox options can't be selected
|
||||
showTooltip={type === 'radio'}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex min-w-[140px] gap-2', {
|
||||
'opacity-30': limitedToBE,
|
||||
'h-full flex-col justify-start': !slim,
|
||||
'slim items-center': slim,
|
||||
})}
|
||||
@@ -83,7 +66,7 @@ export function BoxSelectorItem<T extends Value>({
|
||||
);
|
||||
|
||||
function isDisabled() {
|
||||
return disabled || (limitedToBE && option.disabledWhenLimited);
|
||||
return disabled;
|
||||
}
|
||||
|
||||
function renderIcon() {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Briefcase } from 'lucide-react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
interface Props {
|
||||
url?: string;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
export function LimitedToBeBoxSelectorIndicator({
|
||||
url,
|
||||
showTooltip = true,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="mx-auto flex max-w-fit items-center rounded-b-lg border border-t-0 border-solid border-gray-6 bg-transparent px-3 py-1 text-gray-6">
|
||||
<a
|
||||
className="inline-flex items-center text-xs text-gray-6"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Icon icon={Briefcase} className="!mr-1" />
|
||||
<span>Business Feature</span>
|
||||
</a>
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
size="sm"
|
||||
message="Select this option to preview this business feature."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import type { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
export type Value = number | string | boolean;
|
||||
@@ -13,8 +11,6 @@ export interface BoxSelectorOption<T extends Value> extends IconProps {
|
||||
readonly value: T;
|
||||
readonly disabled?: boolean | (() => boolean);
|
||||
readonly tooltip?: () => string;
|
||||
readonly feature?: FeatureId;
|
||||
readonly disabledWhenLimited?: boolean;
|
||||
readonly hide?: boolean;
|
||||
readonly iconType?: 'raw' | 'badge' | 'logo';
|
||||
readonly iconClass?: string;
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
let globalId = 0;
|
||||
|
||||
interface Props {
|
||||
portalId: HubSpotCreateFormOptions['portalId'];
|
||||
formId: HubSpotCreateFormOptions['formId'];
|
||||
region: HubSpotCreateFormOptions['region'];
|
||||
|
||||
onSubmitted: () => void;
|
||||
|
||||
loading?: ReactNode;
|
||||
}
|
||||
|
||||
export function HubspotForm({
|
||||
loading,
|
||||
portalId,
|
||||
region,
|
||||
formId,
|
||||
onSubmitted,
|
||||
}: Props) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const id = useRef(`reactHubspotForm${globalId++}`);
|
||||
const { isLoading } = useHubspotForm({
|
||||
elId: id.current,
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onSubmitted,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={elRef}
|
||||
id={id.current}
|
||||
style={{ display: isLoading ? 'none' : 'block' }}
|
||||
/>
|
||||
{isLoading && loading}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useHubspotForm({
|
||||
elId,
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onSubmitted,
|
||||
}: {
|
||||
elId: string;
|
||||
portalId: HubSpotCreateFormOptions['portalId'];
|
||||
formId: HubSpotCreateFormOptions['formId'];
|
||||
region: HubSpotCreateFormOptions['region'];
|
||||
|
||||
onSubmitted: () => void;
|
||||
}) {
|
||||
return useQuery(
|
||||
['hubspot', { elId, formId, portalId, region }],
|
||||
async () => {
|
||||
await loadHubspot();
|
||||
await createForm(`#${elId}`, {
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onFormSubmitted: onSubmitted,
|
||||
});
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function loadHubspot() {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (window.hbspt) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement(`script`);
|
||||
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
resolve();
|
||||
};
|
||||
script.src = `//js.hsforms.net/forms/v2.js`;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function createForm(
|
||||
target: string,
|
||||
options: Omit<HubSpotCreateFormOptions, 'target'>
|
||||
) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!window.hbspt) {
|
||||
throw new Error('hbspt object is missing');
|
||||
}
|
||||
|
||||
window.hbspt.forms.create({
|
||||
...options,
|
||||
target,
|
||||
onFormReady(...rest) {
|
||||
options.onFormReady?.(...rest);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { BotMessageSquare } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import headerStyles from './HeaderTitle.module.css';
|
||||
|
||||
const docsUrl = 'https://www.portainer.io/ask-the-ai';
|
||||
|
||||
export function AskAILink() {
|
||||
return (
|
||||
<div className={headerStyles.menuButton}>
|
||||
<a
|
||||
href={docsUrl}
|
||||
target="_blank"
|
||||
color="none"
|
||||
className={clsx(
|
||||
headerStyles.menuIcon,
|
||||
'icon-badge mr-1 cursor-pointer !p-2 text-lg',
|
||||
'text-gray-8',
|
||||
'th-dark:text-gray-warm-7'
|
||||
)}
|
||||
title="Ask AI"
|
||||
rel="noreferrer"
|
||||
data-cy="ask-ai-button"
|
||||
>
|
||||
<BotMessageSquare className="lucide" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { ContextHelp } from '@@/PageHeader/ContextHelp';
|
||||
|
||||
import { useHeaderContext } from './HeaderContainer';
|
||||
import { NotificationsMenu } from './NotificationsMenu';
|
||||
import { UserMenu } from './UserMenu';
|
||||
import { AskAILink } from './AskAILink';
|
||||
|
||||
export function HeaderTitle() {
|
||||
useHeaderContext();
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{isBE && <AskAILink />}
|
||||
<NotificationsMenu />
|
||||
<ContextHelp />
|
||||
{!window.ddExtension && <UserMenu />}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user