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>
This commit is contained in:
claude code agent
2026-06-29 06:33:27 +03:00
parent 76896e5916
commit ef47503bf8
7 changed files with 0 additions and 266 deletions

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

@@ -1,46 +0,0 @@
export enum Edition {
CE,
BE,
}
export enum FeatureState {
HIDDEN,
VISIBLE,
LIMITED_BE,
}
export enum FeatureId {
K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota',
K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota',
K8S_CREATE_FROM_KUBECONFIG = 'k8s-create-from-kubeconfig',
K8S_EDIT_YAML = 'k8s-edit-yaml',
KAAS_PROVISIONING = 'kaas-provisioning',
RBAC_ROLES = 'rbac-roles',
REGISTRY_MANAGEMENT = 'registry-management',
K8S_SETUP_DEFAULT = 'k8s-setup-default',
S3_BACKUP_SETTING = 's3-backup-setting',
S3_RESTORE = 'restore-s3-form',
HIDE_INTERNAL_AUTHENTICATION_PROMPT = 'hide-internal-authentication-prompt',
TEAM_MEMBERSHIP = 'team-membership',
HIDE_INTERNAL_AUTH = 'hide-internal-auth',
EXTERNAL_AUTH_LDAP = 'external-auth-ldap',
ACTIVITY_AUDIT = 'activity-audit',
FORCE_REDEPLOYMENT = 'force-redeployment',
HIDE_AUTO_UPDATE_WINDOW = 'hide-auto-update-window',
IMAGE_UP_TO_DATE_INDICATOR = 'image-up-to-date-indicator',
STACK_PULL_IMAGE = 'stack-pull-image',
STACK_WEBHOOK = 'stack-webhook',
CONTAINER_WEBHOOK = 'container-webhook',
POD_SECURITY_POLICY_CONSTRAINT = 'pod-security-policy-constraint',
HIDE_DOCKER_HUB_ANONYMOUS = 'hide-docker-hub-anonymous',
CUSTOM_LOGIN_BANNER = 'custom-login-banner',
ENFORCE_DEPLOYMENT_OPTIONS = 'k8s-enforce-deployment-options',
K8S_ADM_ONLY_USR_INGRESS_DEPLY = 'k8s-admin-only-ingress-deploy',
K8S_ADM_ONLY_SECRETS = 'k8s-admin-only-secrets',
K8S_ROLLING_RESTART = 'k8s-rolling-restart',
K8SINSTALL = 'k8s-install',
KUBESOLO = 'kubesolo',
K8S_ANNOTATIONS = 'k8s-annotations',
CA_FILE = 'ca-file',
K8S_REQUIRE_NOTE_ON_APPLICATIONS = 'k8s-note-on-applications',
}

View File

@@ -1,62 +0,0 @@
.form-control.limited-be {
border-color: var(--BE-only);
}
.form-control.limited-be.no-border {
border-color: var(--border-form-control-color);
}
button.limited-be,
button[disabled].limited-be.oauth-save-settings-button {
background-color: var(--BE-only);
border-color: var(--BE-only);
margin-left: 0px;
}
button.limited-be.oauth-save-settings-button {
background-color: var(--blue-2);
border-color: transparent;
margin-left: 0px;
}
ng-form.limited-be,
form.limited-be,
div.limited-be {
border: solid 1px var(--BE-only);
border-radius: 8px;
pointer-events: none;
touch-action: none;
display: block;
}
.limited-be-content {
@apply border-gray-6 p-2.5 opacity-50;
}
.limited-be-link {
padding: 10px;
width: inherit;
z-index: 5;
position: absolute;
top: 0px;
right: 0px;
float: right;
border-top-right-radius: 8px;
border-bottom-left-radius: 8px;
touch-action: auto;
cursor: hand;
pointer-events: auto;
}
.limited-be-link a {
@apply text-gray-6;
}
.limited-be-link a:hover {
@apply underline;
@apply border-blue-9 text-blue-9;
}
.form-control.limited-be[disabled] {
background-color: transparent !important;
}

View File

@@ -1,79 +0,0 @@
import { Edition, FeatureId, FeatureState } from './enums';
export const isBE = process.env.PORTAINER_EDITION === 'BE';
interface ServiceState {
currentEdition: Edition;
features: Record<FeatureId, Edition>;
}
const state: ServiceState = {
currentEdition: Edition.CE,
features: <Record<FeatureId, Edition>>{},
};
export async function init(edition: Edition) {
// will be loaded on runtime
const currentEdition = edition;
const features = {
[FeatureId.K8S_RESOURCE_POOL_LB_QUOTA]: Edition.BE,
[FeatureId.K8S_RESOURCE_POOL_STORAGE_QUOTA]: Edition.BE,
[FeatureId.K8S_CREATE_FROM_KUBECONFIG]: Edition.BE,
[FeatureId.KAAS_PROVISIONING]: Edition.BE,
[FeatureId.K8SINSTALL]: Edition.BE,
[FeatureId.KUBESOLO]: Edition.BE,
[FeatureId.ACTIVITY_AUDIT]: Edition.BE,
[FeatureId.EXTERNAL_AUTH_LDAP]: Edition.BE,
[FeatureId.HIDE_INTERNAL_AUTH]: Edition.BE,
[FeatureId.HIDE_INTERNAL_AUTHENTICATION_PROMPT]: Edition.BE,
[FeatureId.K8S_SETUP_DEFAULT]: Edition.BE,
[FeatureId.RBAC_ROLES]: Edition.BE,
[FeatureId.REGISTRY_MANAGEMENT]: Edition.BE,
[FeatureId.S3_BACKUP_SETTING]: Edition.BE,
[FeatureId.S3_RESTORE]: Edition.BE,
[FeatureId.TEAM_MEMBERSHIP]: Edition.BE,
[FeatureId.FORCE_REDEPLOYMENT]: Edition.BE,
[FeatureId.HIDE_AUTO_UPDATE_WINDOW]: Edition.BE,
[FeatureId.IMAGE_UP_TO_DATE_INDICATOR]: Edition.BE,
[FeatureId.STACK_PULL_IMAGE]: Edition.BE,
[FeatureId.STACK_WEBHOOK]: Edition.BE,
[FeatureId.CONTAINER_WEBHOOK]: Edition.BE,
[FeatureId.POD_SECURITY_POLICY_CONSTRAINT]: Edition.BE,
[FeatureId.HIDE_DOCKER_HUB_ANONYMOUS]: Edition.BE,
[FeatureId.CUSTOM_LOGIN_BANNER]: Edition.BE,
[FeatureId.K8S_EDIT_YAML]: Edition.BE,
[FeatureId.ENFORCE_DEPLOYMENT_OPTIONS]: Edition.BE,
[FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE,
[FeatureId.K8S_ADM_ONLY_SECRETS]: Edition.BE,
[FeatureId.K8S_ROLLING_RESTART]: Edition.BE,
[FeatureId.K8S_ANNOTATIONS]: Edition.BE,
[FeatureId.CA_FILE]: Edition.BE,
[FeatureId.K8S_REQUIRE_NOTE_ON_APPLICATIONS]: Edition.BE,
};
state.currentEdition = currentEdition;
state.features = features;
}
export function selectShow(featureId?: FeatureId) {
if (!featureId) {
return FeatureState.VISIBLE;
}
if (!state.features[featureId]) {
return FeatureState.HIDDEN;
}
if (state.features[featureId] <= state.currentEdition) {
return FeatureState.VISIBLE;
}
if (state.features[featureId] === Edition.BE) {
return FeatureState.LIMITED_BE;
}
return FeatureState.HIDDEN;
}
export function isLimitedToBE(featureId?: FeatureId) {
return selectShow(featureId) === FeatureState.LIMITED_BE;
}

View File

@@ -1,8 +0,0 @@
import angular from 'angular';
import { limitedFeatureDirective } from './limited-feature.directive';
import './feature-flags.css';
export default angular
.module('portainer.feature-flags', [])
.directive('limitedFeatureDir', limitedFeatureDirective).name;

View File

@@ -1,44 +0,0 @@
import _ from 'lodash';
import { IAttributes, IDirective, IScope } from 'angular';
import { FeatureState } from '@/react/portainer/feature-flags/enums';
import { selectShow } from './feature-flags.service';
const BASENAME = 'limitedFeature';
/* @ngInject */
export function limitedFeatureDirective(): IDirective {
return {
restrict: 'A',
link,
};
function link(scope: IScope, elem: JQLite, attrs: IAttributes) {
const { limitedFeatureDir: featureId } = attrs;
if (!featureId) {
return;
}
const limitedFeatureAttrs = Object.keys(attrs)
.filter((attr) => attr.startsWith(BASENAME) && attr !== `${BASENAME}Dir`)
.map((attr) => [_.kebabCase(attr.replace(BASENAME, '')), attrs[attr]]);
const state = selectShow(featureId);
if (state === FeatureState.HIDDEN) {
elem.hide();
return;
}
if (state === FeatureState.VISIBLE) {
return;
}
limitedFeatureAttrs.forEach(([attr, value = attr]) => {
const currentValue = elem.attr(attr) || '';
elem.attr(attr, `${currentValue} ${value}`.trim());
});
}
}

View File

@@ -1,22 +0,0 @@
import { ComponentType } from 'react';
export function withEdition<T>(
WrappedComponent: ComponentType<T>,
edition: 'BE' | 'CE'
): ComponentType<T> {
// Try to create a nice displayName for React Dev Tools.
const displayName =
WrappedComponent.displayName || WrappedComponent.name || 'Component';
function WrapperComponent(props: T & JSX.IntrinsicAttributes) {
if (process.env.PORTAINER_EDITION !== edition) {
return null;
}
return <WrappedComponent {...props} />;
}
WrapperComponent.displayName = `with${edition}Edition(${displayName})`;
return WrapperComponent;
}