Compare commits

..

11 Commits

Author SHA1 Message Date
claude code agent
f27e44f5f2 refactor(ce): remove vestigial limitedToBE plumbing from BoxSelector (F8)
After the BE indicator was removed, limitedToBE was hardcoded false and threaded
through BoxSelectorItem.onSelect -> BoxSelector.onChange/handleSelect ->
BoxSelectorAngular.handleChange, where $setValidity(name, !limitedToBE) was a
permanent no-op (always valid). Drop the parameter from the whole chain and the
no-op $setValidity. That left formCtrl/require:'^form'/IFormController dead (they
existed only for that validity call), so remove them too — the component no longer
needs a parent form. The real on-change wiring ($evalAsync -> onChange(value)) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:26:10 +03:00
claude code agent
5f16799b4c refactor(ce): drop orphaned OPEN_LDAP type and collapse degenerate buildUrl (F6,F7)
F6: remove SERVER_TYPES.OPEN_LDAP (read nowhere after the OpenLDAP retirement).
F7: the S3 callers that passed buildUrl(subResource, action) are gone; the only
    remaining caller uses buildUrl() with no args, so collapse it to return 'backup'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:49:32 +03:00
claude code agent
28a06e80a8 refactor(ce): finish dead-BE removal — S3 backup, user-activity, LDAP, edition markers (F1-F5)
Maintainer pre-merge review follow-up (all non-blocker dead code):
F1: delete the dead S3-backup remnants (validation, query hooks, S3-only query
    key, BackupS3Model/Settings types) — kept the CE file-backup path.
F2: delete the orphaned user-activity services + their registration (kept the
    notifications component and routes).
F3: drop the unused buildOpenLDAPSettingsModel().
F4: drop the dead one-option ldap-options data (the selector was already collapsed).
F5: remove the dead data-edition attribute + its process.env typing; silence the
    intentional hasAuthorizations unused-params; drop the dead useRolesState meta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:52:33 +03:00
claude code agent
90f51d48bb refactor(ce): drop orphaned access role column + no-op lodash compact (F6,F7)
F6: delete AccessDatatable/columns/role.tsx — the BE-only role column lost its
    only importer when useColumns stopped importing it; zero importers remain.
F7: useColumns wrapped only always-truthy helper.accessor results in _.compact,
    a no-op; return the plain array and drop the now-dead lodash import (same
    collapse already done in the parallel Wizard column files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:08:01 +03:00
claude code agent
b1b09e5da0 refactor(ce): remove leftover dead BE code — gates, orphans, dead selectors/CSS (F1-F4)
F1: drop the two HomeView edition-gate panels + their files (License/BackupFailed).
F2: delete zero-importer orphans (edition mutation, HubspotForm, HomepageFilter,
    relations mutation, ActivityLogsView cluster, ExperimentalFeatures subtree).
F3: collapse single-option selectors (Backup settings, init restore, env types)
    and delete the option files they orphaned.
F4: remove dead BE-teaser CSS rules and the --BE-only variable.
Also drop the orphaned .btn-warninglight BE-teaser variant.
F5 (limitedToBE) intentionally left — it is still read by BoxSelectorAngular.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:13:55 +03:00
claude code agent
b7df90905d feat(ce): drop OCI "Business Feature" teaser and orphaned BE edge module
Remove the disabled "Installing from an OCI registry is a Portainer
Business Feature" option from the CE Helm repository selector so no
Business Feature teaser remains; CE Helm Repositories options are
unaffected.

Delete the orphaned AutomaticEdgeEnvCreation module (incl.
EnableWaitingRoomSwitch) — its render was already removed from
EdgeComputeSettingsView and nothing imports it anymore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:47:43 +03:00
claude code agent
ef47503bf8 feat(ce): tear down BE edition-gating engine
Delete the feature-flags edition machinery (isBE, init/selectShow/
isLimitedToBE, FeatureId/Edition/FeatureState enums, BEFeatureIndicator,
BEOverlay, BETeaserButton, withEdition, useLimitToBE, limitedFeatureDir)
now that all consumers are gone, drop the initFeatureService bootstrap,
and update tests/stories to assert CE-only behaviour. Mechanism B
(runtime FeatureFlag) and withHideOnExtension are left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:33:27 +03:00
claude code agent
76896e5916 feat(ce): strip BE teaser plumbing from AngularJS layer
Unwrap be-feature-indicator / limited-feature directives and feature-id
attributes from LDAP/OAuth/access-management templates, delete BE-only
AngularJS views (Active Directory, OpenLDAP, RBAC access-viewer, auth
logs) and remove their registrations/routes and the r2a teaser-prop
allow-lists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:33:27 +03:00
claude code agent
7dc98df2b6 feat(ce): remove BE chrome, routes, upsell banner and shared teaser props
Drop the Upgrade-to-Business banner, BE sidebar items (Licenses, Shared
Credentials, Edge Configurations, Waiting Room, Update & Rollback), BE
branding (BE logo/footer), and BE-only routed views (update-schedules,
EdgeAutoCreateScript, WaitingRoom, TimeWindowDisplay/Picker). Prune the
featureId/feature/BEFeatureID teaser props from shared components
(Switch, SwitchField, BoxSelector, TooltipWithChildren, wizard Option)
and fold isBE in useUser while preserving CE authorization semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:33:15 +03:00
claude code agent
cddccd2a5f feat(ce): remove BE teasers from Portainer React views
Collapse isBE/isLimitedToBE consumers and delete BE-only teaser UI in
settings, gitops, registries, custom templates, environments wizard,
access control, activity logs, registries and home/system views. The
Activity Audit view keeps its route but renders a plain CE empty state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:33:15 +03:00
claude code agent
003a90c235 feat(ce): collapse BE edition gating in Docker/Kubernetes/Edge views
Remove always-false isBE branches, BE-only teaser controls and the
now-dead imports across the Docker, Kubernetes and Edge-stack React
views. CE behaviour is preserved; only the Business Edition branches,
teasers and BE-only (non-functional) controls are removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 06:33:03 +03:00
337 changed files with 249 additions and 10963 deletions

View File

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

View File

@@ -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)), [

View File

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

View File

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

View File

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

View File

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

@@ -76,13 +76,3 @@ interface Window {
};
};
}
declare module 'process' {
global {
namespace NodeJS {
interface ProcessEnv {
PORTAINER_EDITION: 'BE' | 'CE';
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
import controller from './BEFeatureIndicator.controller';
export const beFeatureIndicator = {
templateUrl: './BEFeatureIndicator.html',
controller,
bindings: {
feature: '<',
},
transclude: true,
};

View File

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

View File

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

View File

@@ -9,6 +9,5 @@ export const porAccessManagement = {
updateAccess: '<',
actionInProgress: '<',
filterUsers: '<',
limitedFeature: '<',
},
};

View File

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

View File

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

View File

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

View File

@@ -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 = [];
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import controller from './access-viewer.controller';
export const accessViewer = {
templateUrl: './access-viewer.html',
controller,
};

View File

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

View File

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

View File

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

View File

@@ -12,13 +12,8 @@ export const rbacModule = angular
r2a(withUIRouter(withReactQuery(AccessDatatable)), [
'dataset',
'inheritFrom',
'isUpdateEnabled',
'onRemove',
'onUpdate',
'showRoles',
'showWarning',
'tableKey',
'isUpdatingAccess',
'isLoading',
])
).name;

View File

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

View File

@@ -13,7 +13,6 @@ export const switchField = r2a(SwitchField, [
'data-cy',
'disabled',
'onChange',
'featureId',
'switchClass',
'setTooltipHtmlMessage',
'valueExplanation',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '&?',
},
};

View File

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

View File

@@ -4,6 +4,5 @@ export const ldapConnectivityCheck = {
settings: '<',
state: '<',
connectivityCheck: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

@@ -6,6 +6,5 @@ export const ldapCustomGroupSearch = {
bindings: {
settings: '=',
onSearchClick: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

@@ -6,6 +6,5 @@ export const ldapCustomUserSearch = {
bindings: {
settings: '=',
onSearchClick: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

@@ -10,6 +10,5 @@ export const ldapGroupSearchItem = {
baseFilter: '@',
onRemoveClick: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

@@ -9,6 +9,5 @@ export const ldapGroupSearch = {
baseFilter: '@',
onSearchClick: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ export const ldapSettingsSecurity = {
onTlscaCertChange: '<',
uploadInProgress: '<',
title: '@',
limitedFeatureId: '<',
},
controller: LdapController,
};

View File

@@ -3,5 +3,4 @@
on-change="($ctrl.onChangeReactValues)"
upload-state="$ctrl.getUploadState()"
title="$ctrl.title"
limited-feature-id="$ctrl.limitedFeatureId"
></ldap-security-fieldset>

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,5 @@ export const ldapUserSearchItem = {
domainSuffix: '@',
baseFilter: '@',
onRemoveClick: '<',
limitedFeatureId: '<',
},
};

View File

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

View File

@@ -8,7 +8,6 @@ export const ldapUserSearch = {
domainSuffix: '@',
showUsernameFormat: '<',
baseFilter: '@',
limitedFeatureId: '<',
onSearchClick: '<',
},

View File

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

View File

@@ -4,8 +4,6 @@ export const saveAuthSettingsButton = {
onSaveSettings: '<',
saveButtonDisabled: '<',
saveButtonState: '<',
limitedFeatureId: '<',
limitedFeatureClass: '<',
className: '<',
},
};

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import controller from './auth-logs-view.controller.js';
export const authLogsView = {
templateUrl: './auth-logs-view.html',
controller,
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,5 @@
entity-type="endpoint"
inherit-from="ctrl.group"
update-access="ctrl.updateAccess"
limited-feature="ctrl.limitedFeature"
>
</por-access-management>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>[] = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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