mirror of https://github.com/portainer/portainer
refactor(namespace): migrate namespace access view to react [r8s-141] (#87)
parent
8ed7cd80cb
commit
e9fc6d5598
|
@ -489,12 +489,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourcePoolAccess = {
|
const namespaceAccess = {
|
||||||
name: 'kubernetes.resourcePools.resourcePool.access',
|
name: 'kubernetes.resourcePools.resourcePool.access',
|
||||||
url: '/access',
|
url: '/access',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'kubernetesResourcePoolAccessView',
|
component: 'kubernetesNamespaceAccessView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
@ -647,7 +647,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
$stateRegistryProvider.register(resourcePools);
|
$stateRegistryProvider.register(resourcePools);
|
||||||
$stateRegistryProvider.register(namespaceCreation);
|
$stateRegistryProvider.register(namespaceCreation);
|
||||||
$stateRegistryProvider.register(resourcePool);
|
$stateRegistryProvider.register(resourcePool);
|
||||||
$stateRegistryProvider.register(resourcePoolAccess);
|
$stateRegistryProvider.register(namespaceAccess);
|
||||||
$stateRegistryProvider.register(volumes);
|
$stateRegistryProvider.register(volumes);
|
||||||
$stateRegistryProvider.register(volume);
|
$stateRegistryProvider.register(volume);
|
||||||
$stateRegistryProvider.register(registries);
|
$stateRegistryProvider.register(registries);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { KubernetesConfigMap, KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
import { KubernetesConfigMap, KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
||||||
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
||||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
|
||||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
|
import { ConfigurationOwnerUsernameLabel } from '@/react/kubernetes/configs/constants';
|
||||||
class KubernetesConfigMapConverter {
|
class KubernetesConfigMapConverter {
|
||||||
static apiToPortainerAccessConfigMap(data) {
|
static apiToPortainerAccessConfigMap(data) {
|
||||||
const res = new KubernetesPortainerAccessConfigMap();
|
const res = new KubernetesPortainerAccessConfigMap();
|
||||||
|
@ -35,7 +34,7 @@ class KubernetesConfigMapConverter {
|
||||||
res.Id = data.metadata.uid;
|
res.Id = data.metadata.uid;
|
||||||
res.Name = data.metadata.name;
|
res.Name = data.metadata.name;
|
||||||
res.Namespace = data.metadata.namespace;
|
res.Namespace = data.metadata.namespace;
|
||||||
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[ConfigurationOwnerUsernameLabel] : '';
|
||||||
res.CreationDate = data.metadata.creationTimestamp;
|
res.CreationDate = data.metadata.creationTimestamp;
|
||||||
res.Yaml = yaml ? yaml.data : '';
|
res.Yaml = yaml ? yaml.data : '';
|
||||||
res.Labels = data.metadata.labels;
|
res.Labels = data.metadata.labels;
|
||||||
|
@ -79,7 +78,7 @@ class KubernetesConfigMapConverter {
|
||||||
res.metadata.name = data.Name;
|
res.metadata.name = data.Name;
|
||||||
res.metadata.namespace = data.Namespace.Namespace.Name;
|
res.metadata.namespace = data.Namespace.Namespace.Name;
|
||||||
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
res.metadata.labels[ConfigurationOwnerUsernameLabel] = configurationOwner;
|
||||||
|
|
||||||
_.forEach(data.Data, (entry) => {
|
_.forEach(data.Data, (entry) => {
|
||||||
if (entry.IsBinary) {
|
if (entry.IsBinary) {
|
||||||
|
@ -100,7 +99,7 @@ class KubernetesConfigMapConverter {
|
||||||
res.metadata.name = data.Name;
|
res.metadata.name = data.Name;
|
||||||
res.metadata.namespace = data.Namespace;
|
res.metadata.namespace = data.Namespace;
|
||||||
res.metadata.labels = data.Labels || {};
|
res.metadata.labels = data.Labels || {};
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
res.metadata.labels[ConfigurationOwnerUsernameLabel] = data.ConfigurationOwner;
|
||||||
_.forEach(data.Data, (entry) => {
|
_.forEach(data.Data, (entry) => {
|
||||||
if (entry.IsBinary) {
|
if (entry.IsBinary) {
|
||||||
res.binaryData[entry.Key] = entry.Value;
|
res.binaryData[entry.Key] = entry.Value;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash-es';
|
||||||
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
|
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
|
||||||
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
||||||
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
|
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
|
||||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
import { ConfigurationOwnerUsernameLabel } from '@/react/kubernetes/configs/constants';
|
||||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
import { KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
import { KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||||
class KubernetesSecretConverter {
|
class KubernetesSecretConverter {
|
||||||
|
@ -12,7 +12,7 @@ class KubernetesSecretConverter {
|
||||||
res.metadata.namespace = secret.Namespace.Namespace.Name;
|
res.metadata.namespace = secret.Namespace.Namespace.Name;
|
||||||
res.type = secret.Type;
|
res.type = secret.Type;
|
||||||
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
res.metadata.labels[ConfigurationOwnerUsernameLabel] = configurationOwner;
|
||||||
|
|
||||||
let annotation = '';
|
let annotation = '';
|
||||||
_.forEach(secret.Data, (entry) => {
|
_.forEach(secret.Data, (entry) => {
|
||||||
|
@ -40,7 +40,7 @@ class KubernetesSecretConverter {
|
||||||
res.metadata.namespace = secret.Namespace;
|
res.metadata.namespace = secret.Namespace;
|
||||||
res.type = secret.Type;
|
res.type = secret.Type;
|
||||||
res.metadata.labels = secret.Labels || {};
|
res.metadata.labels = secret.Labels || {};
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
res.metadata.labels[ConfigurationOwnerUsernameLabel] = secret.ConfigurationOwner;
|
||||||
|
|
||||||
let annotation = '';
|
let annotation = '';
|
||||||
_.forEach(secret.Data, (entry) => {
|
_.forEach(secret.Data, (entry) => {
|
||||||
|
@ -69,7 +69,7 @@ class KubernetesSecretConverter {
|
||||||
res.Namespace = payload.metadata.namespace;
|
res.Namespace = payload.metadata.namespace;
|
||||||
res.Type = payload.type;
|
res.Type = payload.type;
|
||||||
res.Labels = payload.metadata.labels || {};
|
res.Labels = payload.metadata.labels || {};
|
||||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[ConfigurationOwnerUsernameLabel] : '';
|
||||||
res.CreationDate = payload.metadata.creationTimestamp;
|
res.CreationDate = payload.metadata.creationTimestamp;
|
||||||
res.Annotations = payload.metadata.annotations;
|
res.Annotations = payload.metadata.annotations;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { NamespacesDatatable } from '@/react/kubernetes/namespaces/ListView/NamespacesDatatable';
|
import { NamespacesDatatable } from '@/react/kubernetes/namespaces/ListView/NamespacesDatatable';
|
||||||
import { NamespaceAppsDatatable } from '@/react/kubernetes/namespaces/ItemView/NamespaceAppsDatatable';
|
import { NamespaceAppsDatatable } from '@/react/kubernetes/namespaces/ItemView/NamespaceAppsDatatable';
|
||||||
import { NamespaceAccessDatatable } from '@/react/kubernetes/namespaces/AccessView/AccessDatatable';
|
import { AccessDatatable } from '@/react/kubernetes/namespaces/AccessView/AccessDatatable/AccessDatatable';
|
||||||
|
|
||||||
export const namespacesModule = angular
|
export const namespacesModule = angular
|
||||||
.module('portainer.kubernetes.react.components.namespaces', [])
|
.module('portainer.kubernetes.react.components.namespaces', [])
|
||||||
|
@ -24,8 +24,5 @@ export const namespacesModule = angular
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'namespaceAccessDatatable',
|
'namespaceAccessDatatable',
|
||||||
r2a(withUIRouter(withReactQuery(NamespaceAccessDatatable)), [
|
r2a(withUIRouter(withReactQuery(AccessDatatable)), [])
|
||||||
'dataset',
|
|
||||||
'onRemove',
|
|
||||||
])
|
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { ServiceAccountsView } from '@/react/kubernetes/more-resources/ServiceAc
|
||||||
import { ClusterRolesView } from '@/react/kubernetes/more-resources/ClusterRolesView';
|
import { ClusterRolesView } from '@/react/kubernetes/more-resources/ClusterRolesView';
|
||||||
import { RolesView } from '@/react/kubernetes/more-resources/RolesView';
|
import { RolesView } from '@/react/kubernetes/more-resources/RolesView';
|
||||||
import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView';
|
import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView';
|
||||||
|
import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.kubernetes.react.views', [])
|
.module('portainer.kubernetes.react.views', [])
|
||||||
|
@ -30,6 +31,10 @@ export const viewsModule = angular
|
||||||
'kubernetesNamespacesView',
|
'kubernetesNamespacesView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(NamespacesView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(NamespacesView))), [])
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesNamespaceAccessView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(AccessView))), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesServicesView',
|
'kubernetesServicesView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
|
||||||
|
|
|
@ -3,7 +3,6 @@ import _ from 'lodash-es';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
|
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
|
||||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
import { KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
|
||||||
|
|
||||||
class KubernetesConfigMapService {
|
class KubernetesConfigMapService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -18,22 +17,6 @@ class KubernetesConfigMapService {
|
||||||
this.deleteAsync = this.deleteAsync.bind(this);
|
this.deleteAsync = this.deleteAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccess(namespace, name) {
|
|
||||||
return this.$async(async () => {
|
|
||||||
try {
|
|
||||||
const params = new KubernetesCommonParams();
|
|
||||||
params.id = name;
|
|
||||||
const raw = await this.KubernetesConfigMaps(namespace).get(params).$promise;
|
|
||||||
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(raw);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.status === 404) {
|
|
||||||
return new KubernetesPortainerAccessConfigMap();
|
|
||||||
}
|
|
||||||
throw new PortainerError('Unable to retrieve Portainer accesses', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createAccess(config) {
|
createAccess(config) {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
<page-header
|
|
||||||
ng-if="ctrl.state.viewReady"
|
|
||||||
title="'Namespace access management'"
|
|
||||||
breadcrumbs="[
|
|
||||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
|
||||||
{
|
|
||||||
label:ctrl.pool.Namespace.Name,
|
|
||||||
link: 'kubernetes.resourcePools.resourcePool',
|
|
||||||
linkParams:{id: ctrl.pool.Namespace.Name}
|
|
||||||
},
|
|
||||||
'Access management'
|
|
||||||
]"
|
|
||||||
reload="true"
|
|
||||||
>
|
|
||||||
</page-header>
|
|
||||||
|
|
||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
|
||||||
<div class="row" ng-if="ctrl.pool">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="layers" title-text="Namespace"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<table class="table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>
|
|
||||||
{{ ctrl.pool.Namespace.Name }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget ng-if="ctrl.availableUsersAndTeams">
|
|
||||||
<rd-widget-header icon="user-x" title-text="Create access"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<div class="form-group">
|
|
||||||
<div
|
|
||||||
ng-if="!ctrl.isRBACEnabled"
|
|
||||||
class="small mx-[15px] mb-6 flex gap-1 rounded-lg border border-solid border-warning-5 bg-warning-2 p-4 text-warning-8 th-highcontrast:bg-yellow-11 th-highcontrast:text-white th-dark:bg-yellow-11 th-dark:text-white"
|
|
||||||
>
|
|
||||||
<div class="mt-0.5">
|
|
||||||
<pr-icon icon="'alert-triangle'" feather="true" class-name="'text-warning-7 th-dark:text-white th-highcontrast:text-white'"></pr-icon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p> Your cluster does not have Kubernetes role-based access control (RBAC) enabled. </p>
|
|
||||||
<p> This means you can't use Portainer RBAC functionality to regulate access to environment resources based on user roles. </p>
|
|
||||||
<p class="mb-0">
|
|
||||||
To enable RBAC, start the <a
|
|
||||||
class="th-highcontrast:text-blue-4 th-dark:text-blue-7"
|
|
||||||
href="https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver"
|
|
||||||
target="_blank"
|
|
||||||
>API server</a
|
|
||||||
> with the <code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">--authorization-mode</code> flag set to a
|
|
||||||
comma-separated list that includes <code class="bg-gray-4 th-highcontrast:bg-black th-dark:bg-black">RBAC</code>, for example:
|
|
||||||
<code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">kube-apiserver --authorization-mode=Example1,RBAC,Example2</code>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="col-sm-12 small text-warning">
|
|
||||||
<p class="vertical-center">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
|
||||||
Adding user access will require the affected user(s) to logout and login for the changes to be taken into account.
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="users-selector"> Select user(s) and/or team(s) </label>
|
|
||||||
<div class="col-sm-9 col-lg-4">
|
|
||||||
<span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
|
|
||||||
No user nor team access has been set on the environment. Head over to the
|
|
||||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage them.
|
|
||||||
</span>
|
|
||||||
<namespace-access-users-selector
|
|
||||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
|
||||||
input-id="users-selector"
|
|
||||||
value="ctrl.formValues.multiselectOutput"
|
|
||||||
options="ctrl.availableUsersAndTeams"
|
|
||||||
on-change="(ctrl.onUsersAndTeamsChange)"
|
|
||||||
></namespace-access-users-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- actions -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary btn-sm vertical-center !ml-0"
|
|
||||||
ng-disabled="ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
|
|
||||||
ng-click="ctrl.authorizeAccess()"
|
|
||||||
button-spinner="ctrl.actionInProgress"
|
|
||||||
>
|
|
||||||
<span class="vertical-center" ng-hide="ctrl.state.actionInProgress"><pr-icon icon="'plus'" class="vertical-center"></pr-icon> Create access</span>
|
|
||||||
<span ng-show="ctrl.state.actionInProgress">Creating access...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !actions -->
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<namespace-access-datatable ng-if="ctrl.authorizedUsersAndTeams" dataset="ctrl.authorizedUsersAndTeams" on-remove="(ctrl.unauthorizeAccess)"> </namespace-access-datatable>
|
|
||||||
</div>
|
|
|
@ -1,9 +0,0 @@
|
||||||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolAccessView', {
|
|
||||||
templateUrl: './resourcePoolAccess.html',
|
|
||||||
controller: 'KubernetesResourcePoolAccessController',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
bindings: {
|
|
||||||
$transition$: '<',
|
|
||||||
endpoint: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,145 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash-es';
|
|
||||||
import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
|
|
||||||
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
|
|
||||||
import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
|
||||||
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/getIsRBACEnabled';
|
|
||||||
|
|
||||||
class KubernetesResourcePoolAccessController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $state, $scope, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService, EndpointProvider) {
|
|
||||||
this.$async = $async;
|
|
||||||
this.$state = $state;
|
|
||||||
this.$scope = $scope;
|
|
||||||
this.EndpointProvider = EndpointProvider;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
|
||||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
|
||||||
|
|
||||||
this.GroupService = GroupService;
|
|
||||||
this.AccessService = AccessService;
|
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
|
||||||
this.authorizeAccessAsync = this.authorizeAccessAsync.bind(this);
|
|
||||||
this.unauthorizeAccessAsync = this.unauthorizeAccessAsync.bind(this);
|
|
||||||
this.onUsersAndTeamsChange = this.onUsersAndTeamsChange.bind(this);
|
|
||||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
initAccessConfigMap(configMap) {
|
|
||||||
configMap.Name = KubernetesPortainerConfigMapConfigName;
|
|
||||||
configMap.Namespace = KubernetesPortainerConfigMapNamespace;
|
|
||||||
configMap.Data[KubernetesPortainerConfigMapAccessKey] = {};
|
|
||||||
return configMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
async onInit() {
|
|
||||||
const endpoint = this.endpoint;
|
|
||||||
this.state = {
|
|
||||||
actionInProgress: false,
|
|
||||||
viewReady: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.formValues = {
|
|
||||||
multiselectOutput: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// default to true if error is thrown
|
|
||||||
this.isRBACEnabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = this.$transition$.params().id;
|
|
||||||
let [pool, configMap] = await Promise.all([
|
|
||||||
this.KubernetesResourcePoolService.get(name),
|
|
||||||
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
|
||||||
]);
|
|
||||||
this.isRBACEnabled = await getIsRBACEnabled(this.EndpointProvider.endpointID());
|
|
||||||
const group = await this.GroupService.group(endpoint.GroupId);
|
|
||||||
const roles = [];
|
|
||||||
const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles);
|
|
||||||
this.pool = pool;
|
|
||||||
if (configMap.Id === 0) {
|
|
||||||
configMap = this.initAccessConfigMap(configMap);
|
|
||||||
}
|
|
||||||
configMap = KubernetesConfigMapHelper.parseJSONData(configMap);
|
|
||||||
|
|
||||||
this.authorizedUsersAndTeams = [];
|
|
||||||
this.accessConfigMap = configMap;
|
|
||||||
const poolAccesses = configMap.Data[KubernetesPortainerConfigMapAccessKey][name];
|
|
||||||
if (poolAccesses) {
|
|
||||||
this.authorizedUsersAndTeams = _.filter(endpointAccesses.authorizedUsersAndTeams, (item) => {
|
|
||||||
if (item instanceof UserAccessViewModel && poolAccesses.UserAccessPolicies) {
|
|
||||||
return poolAccesses.UserAccessPolicies[item.Id] !== undefined;
|
|
||||||
} else if (item instanceof TeamAccessViewModel && poolAccesses.TeamAccessPolicies) {
|
|
||||||
return poolAccesses.TeamAccessPolicies[item.Id] !== undefined;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams);
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve namespace information');
|
|
||||||
} finally {
|
|
||||||
this.state.viewReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
return this.$async(this.onInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authorize access
|
|
||||||
*/
|
|
||||||
async authorizeAccessAsync() {
|
|
||||||
try {
|
|
||||||
this.state.actionInProgress = true;
|
|
||||||
const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput);
|
|
||||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
|
||||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
|
||||||
this.Notifications.success('Success', 'Access successfully created');
|
|
||||||
this.$state.reload(this.$state.current);
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to create accesses');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUsersAndTeamsChange(value) {
|
|
||||||
this.$scope.$evalAsync(() => {
|
|
||||||
this.formValues.multiselectOutput = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
authorizeAccess() {
|
|
||||||
return this.$async(this.authorizeAccessAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async unauthorizeAccessAsync(selectedItems) {
|
|
||||||
try {
|
|
||||||
this.state.actionInProgress = true;
|
|
||||||
const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems);
|
|
||||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
|
||||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
|
||||||
this.Notifications.success('Success', 'Access successfully removed');
|
|
||||||
this.$state.reload(this.$state.current);
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to remove accesses');
|
|
||||||
} finally {
|
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unauthorizeAccess(selectedItems) {
|
|
||||||
return this.$async(this.unauthorizeAccessAsync, selectedItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KubernetesResourcePoolAccessController;
|
|
||||||
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolAccessController', KubernetesResourcePoolAccessController);
|
|
|
@ -10,7 +10,9 @@ export function filterNonAdministratorUsers(users: User[]) {
|
||||||
type UserLike = Pick<User, 'Role'>;
|
type UserLike = Pick<User, 'Role'>;
|
||||||
|
|
||||||
// To avoid creating divergence between CE and EE
|
// To avoid creating divergence between CE and EE
|
||||||
// isAdmin checks if the user is portainer admin or edge admin
|
/**
|
||||||
|
* isEdgeAdmin checks if the user is edge admin or admin
|
||||||
|
*/
|
||||||
export function isEdgeAdmin(
|
export function isEdgeAdmin(
|
||||||
user: UserLike | undefined,
|
user: UserLike | undefined,
|
||||||
environment?: Pick<Environment, 'Type'> | null
|
environment?: Pick<Environment, 'Type'> | null
|
||||||
|
@ -23,8 +25,10 @@ export function isEdgeAdmin(
|
||||||
}
|
}
|
||||||
|
|
||||||
// To avoid creating divergence between CE and EE
|
// To avoid creating divergence between CE and EE
|
||||||
// isPureAdmin checks only if the user is portainer admin
|
/**
|
||||||
// See bouncer.IsAdmin and bouncer.PureAdminAccess
|
* isPureAdmin checks if the user is portainer admin.
|
||||||
|
* See bouncer.IsAdmin and bouncer.PureAdminAccess
|
||||||
|
*/
|
||||||
export function isPureAdmin(user?: UserLike): boolean {
|
export function isPureAdmin(user?: UserLike): boolean {
|
||||||
return !!user && user.Role === Role.Admin;
|
return !!user && user.Role === Role.Admin;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { EnvironmentGroupId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { EdgeGroup } from '../../edge-groups/types';
|
import { EdgeGroup } from '../../edge-groups/types';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
|
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { EnvironmentGroupId } from '@/react/portainer/environments/types';
|
||||||
import { useCreateGroupMutation } from '@/react/portainer/environments/environment-groups/queries/useCreateGroupMutation';
|
import { useCreateGroupMutation } from '@/react/portainer/environments/environment-groups/queries/useCreateGroupMutation';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { EnvironmentGroupId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
group: EnvironmentGroupId | null;
|
group: EnvironmentGroupId | null;
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function useIsPureAdmin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the admin status of the user, (admin >= edge admin)
|
* Load the admin status of the user, returning true if the user is edge admin or admin.
|
||||||
* @param forceEnvironmentId to force the environment id, used where the environment id can't be loaded from the router, like sidebar
|
* @param forceEnvironmentId to force the environment id, used where the environment id can't be loaded from the router, like sidebar
|
||||||
* @returns query result with isLoading and isAdmin - isAdmin is true if the user edge admin or admin.
|
* @returns query result with isLoading and isAdmin - isAdmin is true if the user edge admin or admin.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { KeyToPath, Pod, Secret } from 'kubernetes-types/core/v1';
|
||||||
import { Asterisk, Plus } from 'lucide-react';
|
import { Asterisk, Plus } from 'lucide-react';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useSecrets } from '@/react/kubernetes/configs/secret.service';
|
import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
@ -18,7 +18,7 @@ type Props = {
|
||||||
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
||||||
const containerVolumeConfigs = getApplicationVolumeConfigs(app);
|
const containerVolumeConfigs = getApplicationVolumeConfigs(app);
|
||||||
|
|
||||||
const { data: secrets } = useSecrets(useEnvironmentId(), namespace);
|
const { data: secrets } = useK8sSecrets(useEnvironmentId(), namespace);
|
||||||
|
|
||||||
if (containerVolumeConfigs.length === 0) {
|
if (containerVolumeConfigs.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useConfigMaps } from '@/react/kubernetes/configs/configmap.service';
|
import { useK8sConfigMaps } from '@/react/kubernetes/configs/queries/useK8sConfigMaps';
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection/FormSection';
|
import { FormSection } from '@@/form-components/FormSection/FormSection';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
@ -24,7 +24,7 @@ export function ConfigMapsFormSection({
|
||||||
errors,
|
errors,
|
||||||
namespace,
|
namespace,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const configMapsQuery = useConfigMaps(useEnvironmentId(), namespace);
|
const configMapsQuery = useK8sConfigMaps(useEnvironmentId(), namespace);
|
||||||
const configMaps = configMapsQuery.data || [];
|
const configMaps = configMapsQuery.data || [];
|
||||||
|
|
||||||
if (configMapsQuery.isLoading) {
|
if (configMapsQuery.isLoading) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useSecrets } from '@/react/kubernetes/configs/secret.service';
|
import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection/FormSection';
|
import { FormSection } from '@@/form-components/FormSection/FormSection';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
@ -24,7 +24,7 @@ export function SecretsFormSection({
|
||||||
errors,
|
errors,
|
||||||
namespace,
|
namespace,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const secretsQuery = useSecrets(useEnvironmentId(), namespace);
|
const secretsQuery = useK8sSecrets(useEnvironmentId(), namespace);
|
||||||
const secrets = secretsQuery.data || [];
|
const secrets = secretsQuery.data || [];
|
||||||
|
|
||||||
if (secretsQuery.isLoading) {
|
if (secretsQuery.isLoading) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { InsightsBox } from '@@/InsightsBox';
|
||||||
|
|
||||||
import { useIngressControllerClassMapQuery } from '../../ingressClass/useIngressControllerClassMap';
|
import { useIngressControllerClassMapQuery } from '../../ingressClass/useIngressControllerClassMap';
|
||||||
import { IngressControllerClassMap } from '../../ingressClass/types';
|
import { IngressControllerClassMap } from '../../ingressClass/types';
|
||||||
import { useIsRBACEnabledQuery } from '../../getIsRBACEnabled';
|
import { useIsRBACEnabled } from '../../useIsRBACEnabled';
|
||||||
import { getIngressClassesFormValues } from '../../ingressClass/IngressClassDatatable/utils';
|
import { getIngressClassesFormValues } from '../../ingressClass/IngressClassDatatable/utils';
|
||||||
|
|
||||||
import { useStorageClassesFormValues } from './useStorageClasses';
|
import { useStorageClassesFormValues } from './useStorageClasses';
|
||||||
|
@ -102,7 +102,7 @@ function InnerForm({
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
}) {
|
}) {
|
||||||
const { data: isRBACEnabled, ...isRBACEnabledQuery } =
|
const { data: isRBACEnabled, ...isRBACEnabledQuery } =
|
||||||
useIsRBACEnabledQuery(environmentId);
|
useIsRBACEnabled(environmentId);
|
||||||
|
|
||||||
const onChangeControllers = useCallback(
|
const onChangeControllers = useCallback(
|
||||||
(controllerClassMap: IngressControllerClassMap[]) =>
|
(controllerClassMap: IngressControllerClassMap[]) =>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
diff a/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/EnableMetricsInput.tsx b/app/react/kubernetes/cluster/ConfigureView/ConfigureForm/EnableMetricsInput.tsx (rejected hunks)
|
|
||||||
@@ -103,7 +103,10 @@ export function EnableMetricsInput({ value, error, environmentId }: Props) {
|
|
||||||
<TextTip color="red" icon={XCircle}>
|
|
||||||
Unable to reach metrics API. You can enable the metrics-server
|
|
||||||
addon in the{' '}
|
|
||||||
- <Link to="kubernetes.cluster">Cluster Details view</Link>.
|
|
||||||
+ <Link to="kubernetes.cluster" data-cy="cluster-details-view-link">
|
|
||||||
+ Cluster Details view
|
|
||||||
+ </Link>
|
|
||||||
+ .
|
|
||||||
</TextTip>
|
|
||||||
)}
|
|
||||||
{metricsFound === true && (
|
|
|
@ -2,20 +2,20 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { withError } from '@/react-tools/react-query';
|
import { withGlobalError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
export function useIsRBACEnabledQuery(environmentId: EnvironmentId) {
|
export function useIsRBACEnabled(environmentId: EnvironmentId) {
|
||||||
return useQuery<boolean, Error>(
|
return useQuery<boolean, Error>(
|
||||||
['environments', environmentId, 'rbacEnabled'],
|
['environments', environmentId, 'rbacEnabled'],
|
||||||
() => getIsRBACEnabled(environmentId),
|
() => getIsRBACEnabled(environmentId),
|
||||||
{
|
{
|
||||||
enabled: !!environmentId,
|
enabled: !!environmentId,
|
||||||
...withError('Unable to check if RBAC is enabled.'),
|
...withGlobalError('Unable to check if RBAC is enabled.'),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getIsRBACEnabled(environmentId: EnvironmentId) {
|
async function getIsRBACEnabled(environmentId: EnvironmentId) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<boolean>(
|
const { data } = await axios.get<boolean>(
|
||||||
`kubernetes/${environmentId}/rbac_enabled`
|
`kubernetes/${environmentId}/rbac_enabled`
|
|
@ -1,2 +1,11 @@
|
||||||
export const configurationOwnerUsernameLabel =
|
export const ConfigurationOwnerUsernameLabel =
|
||||||
'io.portainer.kubernetes.configuration.owner';
|
'io.portainer.kubernetes.configuration.owner';
|
||||||
|
|
||||||
|
export const ConfigurationOwnerIdLabel =
|
||||||
|
'io.portainer.kubernetes.configuration.owner.id';
|
||||||
|
|
||||||
|
export const PortainerNamespaceAccessesConfigMap = {
|
||||||
|
namespace: 'portainer',
|
||||||
|
configMapName: 'portainer-config',
|
||||||
|
accessKey: 'NamespaceAccessPolicies',
|
||||||
|
};
|
||||||
|
|
|
@ -9,33 +9,37 @@ import { Configuration } from '../types';
|
||||||
import { configMapQueryKeys } from './query-keys';
|
import { configMapQueryKeys } from './query-keys';
|
||||||
import { ConfigMapQueryParams } from './types';
|
import { ConfigMapQueryParams } from './types';
|
||||||
|
|
||||||
export function useConfigMap(
|
export function useConfigMap<T = Configuration>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
configMap: string,
|
configMap: string,
|
||||||
options?: { autoRefreshRate?: number } & ConfigMapQueryParams
|
options?: {
|
||||||
|
autoRefreshRate?: number;
|
||||||
|
select?: (data: Configuration) => T;
|
||||||
|
enabled?: boolean;
|
||||||
|
} & ConfigMapQueryParams
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
configMapQueryKeys.configMap(environmentId, namespace, configMap),
|
configMapQueryKeys.configMap(environmentId, namespace, configMap),
|
||||||
() => getConfigMap(environmentId, namespace, configMap, { withData: true }),
|
() => getConfigMap(environmentId, namespace, configMap, { withData: true }),
|
||||||
{
|
{
|
||||||
...withGlobalError('Unable to retrieve ConfigMaps for cluster'),
|
select: options?.select,
|
||||||
refetchInterval() {
|
enabled: options?.enabled,
|
||||||
return options?.autoRefreshRate ?? false;
|
refetchInterval: () => options?.autoRefreshRate ?? false,
|
||||||
},
|
...withGlobalError(`Unable to retrieve ConfigMap '${configMap}'`),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get a configmap
|
// get a configmap
|
||||||
async function getConfigMap(
|
export async function getConfigMap(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
namespace: string,
|
namespace: string,
|
||||||
configMap: string,
|
configMap: string,
|
||||||
params?: { withData?: boolean }
|
params?: { withData?: boolean }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<Configuration[]>(
|
const { data } = await axios.get<Configuration>(
|
||||||
`/kubernetes/${environmentId}/namespaces/${namespace}/configmaps/${configMap}`,
|
`/kubernetes/${environmentId}/namespaces/${namespace}/configmaps/${configMap}`,
|
||||||
{ params }
|
{ params }
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { ConfigMap, ConfigMapList } from 'kubernetes-types/core/v1';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { parseKubernetesAxiosError } from '../../axiosError';
|
||||||
|
|
||||||
|
export const configMapQueryKeys = {
|
||||||
|
configMaps: (environmentId: EnvironmentId, namespace?: string) => [
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'configmaps',
|
||||||
|
'namespaces',
|
||||||
|
namespace,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a usequery hook for the list of configmaps from the kubernetes API
|
||||||
|
*/
|
||||||
|
export function useK8sConfigMaps(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace?: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
configMapQueryKeys.configMaps(environmentId, namespace),
|
||||||
|
() => (namespace ? getConfigMaps(environmentId, namespace) : []),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError(
|
||||||
|
'Failure',
|
||||||
|
err as Error,
|
||||||
|
`Unable to get ConfigMaps in namespace '${namespace}'`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enabled: !!namespace,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all configmaps for a namespace
|
||||||
|
async function getConfigMaps(environmentId: EnvironmentId, namespace: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<ConfigMapList>(
|
||||||
|
buildUrl(environmentId, namespace)
|
||||||
|
);
|
||||||
|
const configMapsWithKind: ConfigMap[] = data.items.map((configmap) => ({
|
||||||
|
...configmap,
|
||||||
|
kind: 'ConfigMap',
|
||||||
|
}));
|
||||||
|
return configMapsWithKind;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseKubernetesAxiosError(e, 'Unable to retrieve ConfigMaps');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(environmentId: number, namespace: string, name?: string) {
|
||||||
|
const url = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/configmaps`;
|
||||||
|
return name ? `${url}/${name}` : url;
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Secret, SecretList } from 'kubernetes-types/core/v1';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { parseKubernetesAxiosError } from '../../axiosError';
|
||||||
|
|
||||||
|
export const secretQueryKeys = {
|
||||||
|
secrets: (environmentId: EnvironmentId, namespace?: string) => [
|
||||||
|
'environments',
|
||||||
|
environmentId,
|
||||||
|
'kubernetes',
|
||||||
|
'secrets',
|
||||||
|
'namespaces',
|
||||||
|
namespace,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a usequery hook for the list of secrets from the kubernetes API
|
||||||
|
*/
|
||||||
|
export function useK8sSecrets(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace?: string
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
secretQueryKeys.secrets(environmentId, namespace),
|
||||||
|
() => (namespace ? getSecrets(environmentId, namespace) : []),
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
notifyError(
|
||||||
|
'Failure',
|
||||||
|
err as Error,
|
||||||
|
`Unable to get secrets in namespace '${namespace}'`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enabled: !!namespace,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all secrets for a namespace
|
||||||
|
async function getSecrets(environmentId: EnvironmentId, namespace: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<SecretList>(
|
||||||
|
buildUrl(environmentId, namespace)
|
||||||
|
);
|
||||||
|
const secretsWithKind: Secret[] = data.items.map((secret) => ({
|
||||||
|
...secret,
|
||||||
|
kind: 'Secret',
|
||||||
|
}));
|
||||||
|
return secretsWithKind;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseKubernetesAxiosError(e, 'Unable to retrieve secrets');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(environmentId: number, namespace: string, name?: string) {
|
||||||
|
const url = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/secrets`;
|
||||||
|
return name ? `${url}/${name}` : url;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ConfigMap } from 'kubernetes-types/core/v1';
|
||||||
|
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { parseKubernetesAxiosError } from '../../axiosError';
|
||||||
|
|
||||||
|
import { configMapQueryKeys } from './useK8sConfigMaps';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useUpdateK8sConfigMapMutation returns a mutation hook for updating a Kubernetes ConfigMap using the Kubernetes proxy API.
|
||||||
|
*/
|
||||||
|
export function useUpdateK8sConfigMapMutation(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string
|
||||||
|
) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
data,
|
||||||
|
configMapName,
|
||||||
|
}: {
|
||||||
|
data: ConfigMap;
|
||||||
|
configMapName: string;
|
||||||
|
}) => updateConfigMap(environmentId, namespace, configMapName, data),
|
||||||
|
...withInvalidate(queryClient, [
|
||||||
|
configMapQueryKeys.configMaps(environmentId, namespace),
|
||||||
|
]),
|
||||||
|
// handle success notifications in the calling component
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateConfigMap(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
configMap: string,
|
||||||
|
data: ConfigMap
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await axios.put(
|
||||||
|
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/configmaps/${configMap}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseKubernetesAxiosError(
|
||||||
|
e,
|
||||||
|
`Unable to update ConfigMap '${configMap}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,157 +0,0 @@
|
||||||
import { Secret, SecretList } from 'kubernetes-types/core/v1';
|
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { queryClient, withError } from '@/react-tools/react-query';
|
|
||||||
import axios from '@/portainer/services/axios';
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
||||||
import {
|
|
||||||
error as notifyError,
|
|
||||||
notifySuccess,
|
|
||||||
} from '@/portainer/services/notifications';
|
|
||||||
import { isFulfilled, isRejected } from '@/portainer/helpers/promise-utils';
|
|
||||||
import { pluralize } from '@/portainer/helpers/strings';
|
|
||||||
|
|
||||||
import { parseKubernetesAxiosError } from '../axiosError';
|
|
||||||
|
|
||||||
export const secretQueryKeys = {
|
|
||||||
secrets: (environmentId: EnvironmentId, namespace?: string) => [
|
|
||||||
'environments',
|
|
||||||
environmentId,
|
|
||||||
'kubernetes',
|
|
||||||
'secrets',
|
|
||||||
'namespaces',
|
|
||||||
namespace,
|
|
||||||
],
|
|
||||||
secretsForCluster: (environmentId: EnvironmentId) => [
|
|
||||||
'environments',
|
|
||||||
environmentId,
|
|
||||||
'kubernetes',
|
|
||||||
'secrets',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns a usequery hook for the list of secrets from the kubernetes API
|
|
||||||
export function useSecrets(environmentId: EnvironmentId, namespace?: string) {
|
|
||||||
return useQuery(
|
|
||||||
secretQueryKeys.secrets(environmentId, namespace),
|
|
||||||
() => (namespace ? getSecrets(environmentId, namespace) : []),
|
|
||||||
{
|
|
||||||
onError: (err) => {
|
|
||||||
notifyError(
|
|
||||||
'Failure',
|
|
||||||
err as Error,
|
|
||||||
`Unable to get secrets in namespace '${namespace}'`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enabled: !!namespace,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSecretsForCluster(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
namespaces?: string[],
|
|
||||||
options?: { autoRefreshRate?: number }
|
|
||||||
) {
|
|
||||||
return useQuery(
|
|
||||||
secretQueryKeys.secretsForCluster(environmentId),
|
|
||||||
() => namespaces && getSecretsForCluster(environmentId, namespaces),
|
|
||||||
{
|
|
||||||
...withError('Unable to retrieve secrets for cluster'),
|
|
||||||
enabled: !!namespaces?.length,
|
|
||||||
refetchInterval() {
|
|
||||||
return options?.autoRefreshRate ?? false;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMutationDeleteSecrets(environmentId: EnvironmentId) {
|
|
||||||
return useMutation(
|
|
||||||
async (secrets: { namespace: string; name: string }[]) => {
|
|
||||||
const promises = await Promise.allSettled(
|
|
||||||
secrets.map(({ namespace, name }) =>
|
|
||||||
deleteSecret(environmentId, namespace, name)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const successfulSecrets = promises
|
|
||||||
.filter(isFulfilled)
|
|
||||||
.map((_, index) => secrets[index].name);
|
|
||||||
const failedSecrets = promises
|
|
||||||
.filter(isRejected)
|
|
||||||
.map(({ reason }, index) => ({
|
|
||||||
name: secrets[index].name,
|
|
||||||
reason,
|
|
||||||
}));
|
|
||||||
return { failedSecrets, successfulSecrets };
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...withError('Unable to remove secrets'),
|
|
||||||
onSuccess: ({ failedSecrets, successfulSecrets }) => {
|
|
||||||
queryClient.invalidateQueries(
|
|
||||||
secretQueryKeys.secretsForCluster(environmentId)
|
|
||||||
);
|
|
||||||
// show an error message for each secret that failed to delete
|
|
||||||
failedSecrets.forEach(({ name, reason }) => {
|
|
||||||
notifyError(
|
|
||||||
`Failed to remove secret '${name}'`,
|
|
||||||
new Error(reason.message) as Error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// show one summary message for all successful deletes
|
|
||||||
if (successfulSecrets.length) {
|
|
||||||
notifySuccess(
|
|
||||||
`${pluralize(
|
|
||||||
successfulSecrets.length,
|
|
||||||
'Secret'
|
|
||||||
)} successfully removed`,
|
|
||||||
successfulSecrets.join(', ')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSecretsForCluster(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
namespaces: string[]
|
|
||||||
) {
|
|
||||||
const secrets = await Promise.all(
|
|
||||||
namespaces.map((namespace) => getSecrets(environmentId, namespace))
|
|
||||||
);
|
|
||||||
return secrets.flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all secrets for a namespace
|
|
||||||
async function getSecrets(environmentId: EnvironmentId, namespace: string) {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get<SecretList>(
|
|
||||||
buildUrl(environmentId, namespace)
|
|
||||||
);
|
|
||||||
const secretsWithKind: Secret[] = data.items.map((secret) => ({
|
|
||||||
...secret,
|
|
||||||
kind: 'Secret',
|
|
||||||
}));
|
|
||||||
return secretsWithKind;
|
|
||||||
} catch (e) {
|
|
||||||
throw parseKubernetesAxiosError(e, 'Unable to retrieve secrets');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSecret(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
namespace: string,
|
|
||||||
name: string
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await axios.delete(buildUrl(environmentId, namespace, name));
|
|
||||||
} catch (e) {
|
|
||||||
throw parseKubernetesAxiosError(e, 'Unable to remove secret');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUrl(environmentId: number, namespace: string, name?: string) {
|
|
||||||
const url = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/secrets`;
|
|
||||||
return name ? `${url}/${name}` : url;
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useSecrets } from '@/react/kubernetes/configs/secret.service';
|
import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
|
||||||
import { useNamespaceServices } from '@/react/kubernetes/networks/services/queries';
|
import { useNamespaceServices } from '@/react/kubernetes/networks/services/queries';
|
||||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||||
|
@ -70,7 +70,7 @@ export function CreateIngressView() {
|
||||||
useNamespacesQuery(environmentId);
|
useNamespacesQuery(environmentId);
|
||||||
|
|
||||||
const { data: allServices } = useNamespaceServices(environmentId, namespace);
|
const { data: allServices } = useNamespaceServices(environmentId, namespace);
|
||||||
const secretsResults = useSecrets(environmentId, namespace);
|
const secretsResults = useK8sSecrets(environmentId, namespace);
|
||||||
const ingressesResults = useIngresses(environmentId);
|
const ingressesResults = useIngresses(environmentId);
|
||||||
const { data: ingressControllers, ...ingressControllersQuery } =
|
const { data: ingressControllers, ...ingressControllersQuery } =
|
||||||
useIngressControllers(environmentId, namespace);
|
useIngressControllers(environmentId, namespace);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { UserX } from 'lucide-react';
|
|
||||||
|
|
||||||
import { name } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/columns/name';
|
|
||||||
import { type } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/columns/type';
|
|
||||||
import { Access } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/types';
|
|
||||||
import { RemoveAccessButton } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/RemoveAccessButton';
|
|
||||||
|
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
|
||||||
import { Datatable } from '@@/datatables';
|
|
||||||
|
|
||||||
const tableKey = 'kubernetes_resourcepool_access';
|
|
||||||
const columns = [name, type];
|
|
||||||
const store = createPersistedStore(tableKey);
|
|
||||||
|
|
||||||
export function NamespaceAccessDatatable({
|
|
||||||
dataset,
|
|
||||||
onRemove,
|
|
||||||
}: {
|
|
||||||
dataset?: Array<Access>;
|
|
||||||
onRemove(items: Array<Access>): void;
|
|
||||||
}) {
|
|
||||||
const tableState = useTableState(store, tableKey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Datatable
|
|
||||||
data-cy="kube-namespace-access-datatable"
|
|
||||||
title="Namespace Access"
|
|
||||||
titleIcon={UserX}
|
|
||||||
dataset={dataset || []}
|
|
||||||
isLoading={!dataset}
|
|
||||||
columns={columns}
|
|
||||||
settingsManager={tableState}
|
|
||||||
renderTableActions={(selectedItems) => (
|
|
||||||
<RemoveAccessButton items={selectedItems} onClick={onRemove} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { UserX } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useUsers } from '@/portainer/users/queries';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||||
|
import { PortainerNamespaceAccessesConfigMap } from '@/react/kubernetes/configs/constants';
|
||||||
|
import { useConfigMap } from '@/react/kubernetes/configs/queries/useConfigMap';
|
||||||
|
import { useUpdateK8sConfigMapMutation } from '@/react/kubernetes/configs/queries/useUpdateK8sConfigMapMutation';
|
||||||
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
import { Datatable } from '@@/datatables';
|
||||||
|
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||||
|
|
||||||
|
import { parseNamespaceAccesses } from '../parseNamespaceAccesses';
|
||||||
|
import { NamespaceAccess } from '../types';
|
||||||
|
import { createUnauthorizeAccessConfigMapPayload } from '../createAccessConfigMapPayload';
|
||||||
|
|
||||||
|
import { entityType } from './columns/type';
|
||||||
|
import { name } from './columns/name';
|
||||||
|
|
||||||
|
const tableKey = 'kubernetes_resourcepool_access';
|
||||||
|
const columns = [name, entityType];
|
||||||
|
const store = createPersistedStore(tableKey);
|
||||||
|
|
||||||
|
export function AccessDatatable() {
|
||||||
|
const {
|
||||||
|
params: { id: namespaceName },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const tableState = useTableState(store, tableKey);
|
||||||
|
const usersQuery = useUsers(false, environmentId);
|
||||||
|
const teamsQuery = useTeams(false, environmentId);
|
||||||
|
const accessConfigMapQuery = useConfigMap(
|
||||||
|
environmentId,
|
||||||
|
PortainerNamespaceAccessesConfigMap.namespace,
|
||||||
|
PortainerNamespaceAccessesConfigMap.configMapName
|
||||||
|
);
|
||||||
|
const namespaceAccesses = useMemo(
|
||||||
|
() =>
|
||||||
|
parseNamespaceAccesses(
|
||||||
|
accessConfigMapQuery.data ?? null,
|
||||||
|
namespaceName,
|
||||||
|
usersQuery.data ?? [],
|
||||||
|
teamsQuery.data ?? []
|
||||||
|
),
|
||||||
|
[accessConfigMapQuery.data, usersQuery.data, teamsQuery.data, namespaceName]
|
||||||
|
);
|
||||||
|
const configMap = accessConfigMapQuery.data;
|
||||||
|
|
||||||
|
const updateConfigMapMutation = useUpdateK8sConfigMapMutation(
|
||||||
|
environmentId,
|
||||||
|
PortainerNamespaceAccessesConfigMap.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Datatable
|
||||||
|
data-cy="kube-namespace-access-datatable"
|
||||||
|
title="Namespace access"
|
||||||
|
titleIcon={UserX}
|
||||||
|
dataset={namespaceAccesses}
|
||||||
|
isLoading={accessConfigMapQuery.isLoading}
|
||||||
|
columns={columns}
|
||||||
|
settingsManager={tableState}
|
||||||
|
// the user id and team id can be the same, so add the type to the id
|
||||||
|
getRowId={(row) => `${row.type}-${row.id}`}
|
||||||
|
renderTableActions={(selectedItems) => (
|
||||||
|
<DeleteButton
|
||||||
|
isLoading={updateConfigMapMutation.isLoading}
|
||||||
|
loadingText="Removing..."
|
||||||
|
confirmMessage="Are you sure you want to unauthorized the selected users or teams?"
|
||||||
|
onConfirmed={() => handleUpdate(selectedItems)}
|
||||||
|
disabled={
|
||||||
|
selectedItems.length === 0 ||
|
||||||
|
usersQuery.isLoading ||
|
||||||
|
teamsQuery.isLoading ||
|
||||||
|
accessConfigMapQuery.isLoading
|
||||||
|
}
|
||||||
|
data-cy="remove-access-button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleUpdate(selectedItemsToRemove: Array<NamespaceAccess>) {
|
||||||
|
try {
|
||||||
|
const configMapPayload = createUnauthorizeAccessConfigMapPayload(
|
||||||
|
namespaceAccesses,
|
||||||
|
selectedItemsToRemove,
|
||||||
|
namespaceName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
await updateConfigMapMutation.mutateAsync({
|
||||||
|
data: configMapPayload,
|
||||||
|
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
|
});
|
||||||
|
notifySuccess('Success', 'Namespace access updated');
|
||||||
|
router.stateService.reload();
|
||||||
|
} catch (error) {
|
||||||
|
notifyError('Failed to update namespace access', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { NamespaceAccess } from '../../types';
|
||||||
|
|
||||||
|
export const helper = createColumnHelper<NamespaceAccess>();
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { helper } from './helper';
|
||||||
|
|
||||||
|
export const name = helper.accessor('name', {
|
||||||
|
header: 'Name',
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { helper } from './helper';
|
||||||
|
|
||||||
|
export const entityType = helper.accessor('type', {
|
||||||
|
header: 'Type',
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useUnauthorizedRedirect } from '@/react/hooks/useUnauthorizedRedirect';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
import { NamespaceDetailsWidget } from './NamespaceDetailsWidget';
|
||||||
|
import { AccessDatatable } from './AccessDatatable/AccessDatatable';
|
||||||
|
import { CreateAccessWidget } from './CreateAccessWidget/CreateAccessWidget';
|
||||||
|
|
||||||
|
export function AccessView() {
|
||||||
|
const {
|
||||||
|
params: { id: namespaceName },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
useUnauthorizedRedirect(
|
||||||
|
{ authorizations: ['K8sResourcePoolDetailsW'] },
|
||||||
|
{ to: 'kubernetes.resourcePools' }
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Namespace access management"
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: 'Namespaces', link: 'kubernetes.resourcePools' },
|
||||||
|
{
|
||||||
|
label: namespaceName,
|
||||||
|
link: 'kubernetes.resourcePools.resourcePool',
|
||||||
|
linkParams: { id: namespaceName },
|
||||||
|
},
|
||||||
|
'Access management',
|
||||||
|
]}
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
<NamespaceDetailsWidget />
|
||||||
|
<CreateAccessWidget />
|
||||||
|
<AccessDatatable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { Form, FormikProps } from 'formik';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
|
import { useGroup } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
|
import { useUsers } from '@/portainer/users/queries';
|
||||||
|
import { useTeams } from '@/react/portainer/users/teams/queries/useTeams';
|
||||||
|
import { User } from '@/portainer/users/types';
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { Team } from '@/react/portainer/users/teams/types';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
|
||||||
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
|
import { LoadingButton } from '@@/buttons';
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
import { NamespaceAccessUsersSelector } from '../NamespaceAccessUsersSelector';
|
||||||
|
import { EnvironmentAccess, NamespaceAccess } from '../types';
|
||||||
|
|
||||||
|
import { CreateAccessValues } from './types';
|
||||||
|
|
||||||
|
export function CreateAccessInnerForm({
|
||||||
|
values,
|
||||||
|
handleSubmit,
|
||||||
|
setFieldValue,
|
||||||
|
isSubmitting,
|
||||||
|
isValid,
|
||||||
|
dirty,
|
||||||
|
namespaceAccessesGranted,
|
||||||
|
}: FormikProps<CreateAccessValues> & {
|
||||||
|
namespaceAccessesGranted: NamespaceAccess[];
|
||||||
|
}) {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const environmentQuery = useEnvironment(environmentId);
|
||||||
|
const groupQuery = useGroup(environmentQuery.data?.GroupId);
|
||||||
|
const usersQuery = useUsers(false, environmentId);
|
||||||
|
const teamsQuery = useTeams();
|
||||||
|
const availableTeamOrUserOptions: EnvironmentAccess[] =
|
||||||
|
useAvailableTeamOrUserOptions(
|
||||||
|
values.selectedUsersAndTeams,
|
||||||
|
namespaceAccessesGranted,
|
||||||
|
environmentQuery.data,
|
||||||
|
groupQuery.data,
|
||||||
|
usersQuery.data,
|
||||||
|
teamsQuery.data
|
||||||
|
);
|
||||||
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
return (
|
||||||
|
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
|
||||||
|
<FormControl label="Select user(s) and/or team(s)">
|
||||||
|
{availableTeamOrUserOptions.length > 0 ||
|
||||||
|
values.selectedUsersAndTeams.length > 0 ? (
|
||||||
|
<NamespaceAccessUsersSelector
|
||||||
|
inputId="users-selector"
|
||||||
|
options={availableTeamOrUserOptions}
|
||||||
|
onChange={(opts) => setFieldValue('selectedUsersAndTeams', opts)}
|
||||||
|
value={values.selectedUsersAndTeams}
|
||||||
|
dataCy="namespaceAccess-usersSelector"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="small text-muted pt-2">
|
||||||
|
No user or team access has been set on the environment.
|
||||||
|
{isAdminQuery.isAdmin && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
Head over to the{' '}
|
||||||
|
<Link
|
||||||
|
to="portainer.endpoints"
|
||||||
|
data-cy="namespaceAccess-environmentsLink"
|
||||||
|
>
|
||||||
|
Environments view
|
||||||
|
</Link>{' '}
|
||||||
|
to manage them.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<div className="form-group mt-5">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<LoadingButton
|
||||||
|
disabled={!isValid || !dirty}
|
||||||
|
data-cy="namespaceAccess-createAccessButton"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
loadingText="Creating access..."
|
||||||
|
icon={Plus}
|
||||||
|
className="!ml-0"
|
||||||
|
>
|
||||||
|
Create access
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the team and user options that can be added to the namespace, excluding the ones that already have access.
|
||||||
|
*/
|
||||||
|
function useAvailableTeamOrUserOptions(
|
||||||
|
selectedAccesses: EnvironmentAccess[],
|
||||||
|
namespaceAccessesGranted: NamespaceAccess[],
|
||||||
|
environment?: Environment,
|
||||||
|
group?: EnvironmentGroup,
|
||||||
|
users?: User[],
|
||||||
|
teams?: Team[]
|
||||||
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
|
// get unique users and teams from environment accesses (the keys are the IDs)
|
||||||
|
const environmentAccessPolicies = environment?.UserAccessPolicies ?? {};
|
||||||
|
const environmentTeamAccessPolicies = environment?.TeamAccessPolicies ?? {};
|
||||||
|
const environmentGroupAccessPolicies = group?.UserAccessPolicies ?? {};
|
||||||
|
const environmentGroupTeamAccessPolicies = group?.TeamAccessPolicies ?? {};
|
||||||
|
|
||||||
|
// get all users that have access to the environment
|
||||||
|
const userAccessPolicies = {
|
||||||
|
...environmentAccessPolicies,
|
||||||
|
...environmentGroupAccessPolicies,
|
||||||
|
};
|
||||||
|
const uniqueUserIds = new Set(Object.keys(userAccessPolicies));
|
||||||
|
const userAccessOptions: EnvironmentAccess[] = Array.from(uniqueUserIds)
|
||||||
|
.map((id) => {
|
||||||
|
const userId = parseInt(id, 10);
|
||||||
|
const user = users?.find((u) => u.Id === userId);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// role from the userAccessPolicies is used by default, if not found, role from the environmentTeamAccessPolicies is used
|
||||||
|
const userAccessPolicy =
|
||||||
|
environmentAccessPolicies[userId] ??
|
||||||
|
environmentGroupAccessPolicies[userId];
|
||||||
|
const userAccess: EnvironmentAccess = {
|
||||||
|
id: user?.Id,
|
||||||
|
name: user?.Username,
|
||||||
|
type: 'user',
|
||||||
|
role: {
|
||||||
|
name: 'Standard user',
|
||||||
|
id: userAccessPolicy?.RoleId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return userAccess;
|
||||||
|
})
|
||||||
|
.filter((u) => u !== null);
|
||||||
|
|
||||||
|
// get all teams that have access to the environment
|
||||||
|
const teamAccessPolicies = {
|
||||||
|
...environmentTeamAccessPolicies,
|
||||||
|
...environmentGroupTeamAccessPolicies,
|
||||||
|
};
|
||||||
|
const uniqueTeamIds = new Set(Object.keys(teamAccessPolicies));
|
||||||
|
const teamAccessOptions: EnvironmentAccess[] = Array.from(uniqueTeamIds)
|
||||||
|
.map((id) => {
|
||||||
|
const teamId = parseInt(id, 10);
|
||||||
|
const team = teams?.find((t) => t.Id === teamId);
|
||||||
|
if (!team) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const teamAccessPolicy =
|
||||||
|
environmentTeamAccessPolicies[teamId] ??
|
||||||
|
environmentGroupTeamAccessPolicies[teamId];
|
||||||
|
const teamAccess: EnvironmentAccess = {
|
||||||
|
id: team?.Id,
|
||||||
|
name: team?.Name,
|
||||||
|
type: 'team',
|
||||||
|
role: {
|
||||||
|
name: 'Standard user',
|
||||||
|
id: teamAccessPolicy?.RoleId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return teamAccess;
|
||||||
|
})
|
||||||
|
.filter((t) => t !== null);
|
||||||
|
|
||||||
|
// filter out users and teams that already have access to the namespace
|
||||||
|
const userAndTeamEnvironmentAccesses = [
|
||||||
|
...userAccessOptions,
|
||||||
|
...teamAccessOptions,
|
||||||
|
];
|
||||||
|
const filteredAccessOptions = userAndTeamEnvironmentAccesses.filter(
|
||||||
|
(t) =>
|
||||||
|
!selectedAccesses.some((e) => e.id === t.id && e.type === t.type) &&
|
||||||
|
!namespaceAccessesGranted.some(
|
||||||
|
(e) => e.id === t.id && e.type === t.type
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredAccessOptions;
|
||||||
|
}, [
|
||||||
|
namespaceAccessesGranted,
|
||||||
|
selectedAccesses,
|
||||||
|
environment,
|
||||||
|
group,
|
||||||
|
users,
|
||||||
|
teams,
|
||||||
|
]);
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { UserPlusIcon } from 'lucide-react';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useIsRBACEnabled } from '@/react/kubernetes/cluster/useIsRBACEnabled';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
import { RBACAlert } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/RBACAlert';
|
||||||
|
import { useUsers } from '@/portainer/users/queries';
|
||||||
|
import { PortainerNamespaceAccessesConfigMap } from '@/react/kubernetes/configs/constants';
|
||||||
|
import { useConfigMap } from '@/react/kubernetes/configs/queries/useConfigMap';
|
||||||
|
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||||
|
import { useUpdateK8sConfigMapMutation } from '@/react/kubernetes/configs/queries/useUpdateK8sConfigMapMutation';
|
||||||
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
import { EnvironmentAccess } from '../types';
|
||||||
|
import { createAuthorizeAccessConfigMapPayload } from '../createAccessConfigMapPayload';
|
||||||
|
import { parseNamespaceAccesses } from '../parseNamespaceAccesses';
|
||||||
|
|
||||||
|
import { CreateAccessValues } from './types';
|
||||||
|
import { CreateAccessInnerForm } from './CreateAccessInnerForm';
|
||||||
|
import { validationSchema } from './createAccess.validation';
|
||||||
|
|
||||||
|
export function CreateAccessWidget() {
|
||||||
|
const {
|
||||||
|
params: { id: namespaceName },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const isRBACEnabledQuery = useIsRBACEnabled(environmentId);
|
||||||
|
const initialValues: {
|
||||||
|
selectedUsersAndTeams: EnvironmentAccess[];
|
||||||
|
} = {
|
||||||
|
selectedUsersAndTeams: [],
|
||||||
|
};
|
||||||
|
const usersQuery = useUsers(false, environmentId);
|
||||||
|
const teamsQuery = useTeams(false, environmentId);
|
||||||
|
const accessConfigMapQuery = useConfigMap(
|
||||||
|
environmentId,
|
||||||
|
PortainerNamespaceAccessesConfigMap.namespace,
|
||||||
|
PortainerNamespaceAccessesConfigMap.configMapName
|
||||||
|
);
|
||||||
|
const namespaceAccesses = useMemo(
|
||||||
|
() =>
|
||||||
|
parseNamespaceAccesses(
|
||||||
|
accessConfigMapQuery.data ?? null,
|
||||||
|
namespaceName,
|
||||||
|
usersQuery.data ?? [],
|
||||||
|
teamsQuery.data ?? []
|
||||||
|
),
|
||||||
|
[accessConfigMapQuery.data, usersQuery.data, teamsQuery.data, namespaceName]
|
||||||
|
);
|
||||||
|
const configMap = accessConfigMapQuery.data;
|
||||||
|
|
||||||
|
const updateConfigMapMutation = useUpdateK8sConfigMapMutation(
|
||||||
|
environmentId,
|
||||||
|
PortainerNamespaceAccessesConfigMap.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget aria-label="Create access">
|
||||||
|
<WidgetTitle icon={UserPlusIcon} title="Create access" />
|
||||||
|
<WidgetBody>
|
||||||
|
{isRBACEnabledQuery.data === false && <RBACAlert />}
|
||||||
|
<TextTip className="mb-2" childrenWrapperClassName="text-warning">
|
||||||
|
Adding user access will require the affected user(s) to logout and
|
||||||
|
login for the changes to be taken into account.
|
||||||
|
</TextTip>
|
||||||
|
{isRBACEnabledQuery.data !== false && (
|
||||||
|
<Formik<CreateAccessValues>
|
||||||
|
initialValues={initialValues}
|
||||||
|
enableReinitialize
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
validateOnMount
|
||||||
|
>
|
||||||
|
{(formikProps) => (
|
||||||
|
<CreateAccessInnerForm
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...formikProps}
|
||||||
|
namespaceAccessesGranted={namespaceAccesses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
async function onSubmit(
|
||||||
|
values: {
|
||||||
|
selectedUsersAndTeams: EnvironmentAccess[];
|
||||||
|
},
|
||||||
|
{ resetForm }: { resetForm: () => void }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const configMapPayload = createAuthorizeAccessConfigMapPayload(
|
||||||
|
namespaceAccesses,
|
||||||
|
values.selectedUsersAndTeams,
|
||||||
|
namespaceName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
await updateConfigMapMutation.mutateAsync({
|
||||||
|
data: configMapPayload,
|
||||||
|
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
|
});
|
||||||
|
notifySuccess('Success', 'Namespace access updated');
|
||||||
|
resetForm();
|
||||||
|
} catch (error) {
|
||||||
|
notifyError('Failed to update namespace access', error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { object, array, string, number, SchemaOf, mixed } from 'yup';
|
||||||
|
|
||||||
|
import { CreateAccessValues } from './types';
|
||||||
|
|
||||||
|
export function validationSchema(): SchemaOf<CreateAccessValues> {
|
||||||
|
return object().shape({
|
||||||
|
selectedUsersAndTeams: array(
|
||||||
|
object().shape({
|
||||||
|
type: mixed().oneOf(['team', 'user']).required(),
|
||||||
|
name: string().required(),
|
||||||
|
id: number().required(),
|
||||||
|
role: object().shape({
|
||||||
|
id: number().required(),
|
||||||
|
name: string().required(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
).min(1),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { EnvironmentAccess } from '../types';
|
||||||
|
|
||||||
|
export type CreateAccessValues = {
|
||||||
|
selectedUsersAndTeams: EnvironmentAccess[];
|
||||||
|
};
|
|
@ -3,14 +3,13 @@ import { OptionProps, components, MultiValueGenericProps } from 'react-select';
|
||||||
|
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
|
|
||||||
type Role = { Name: string };
|
import { EnvironmentAccess } from './types';
|
||||||
type Option = { Type: 'user' | 'team'; Id: number; Name: string; Role: Role };
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
value: Option[];
|
value: EnvironmentAccess[];
|
||||||
onChange(value: readonly Option[]): void;
|
onChange(value: readonly EnvironmentAccess[]): void;
|
||||||
options: Option[];
|
options: EnvironmentAccess[];
|
||||||
dataCy: string;
|
dataCy: string;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -29,8 +28,8 @@ export function NamespaceAccessUsersSelector({
|
||||||
<Select
|
<Select
|
||||||
isMulti
|
isMulti
|
||||||
name={name}
|
name={name}
|
||||||
getOptionLabel={(option) => option.Name}
|
getOptionLabel={(option) => option.name}
|
||||||
getOptionValue={(option) => `${option.Id}-${option.Type}`}
|
getOptionValue={(option) => `${option.id}-${option.type}`}
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
|
@ -43,11 +42,14 @@ export function NamespaceAccessUsersSelector({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOption(option: unknown): option is Option {
|
function isOption(option: unknown): option is EnvironmentAccess {
|
||||||
return !!option && typeof option === 'object' && 'Type' in option;
|
return !!option && typeof option === 'object' && 'type' in option;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OptionComponent({ data, ...props }: OptionProps<Option, true>) {
|
function OptionComponent({
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: OptionProps<EnvironmentAccess, true>) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<components.Option data={data} {...props}>
|
<components.Option data={data} {...props}>
|
||||||
|
@ -59,7 +61,7 @@ function OptionComponent({ data, ...props }: OptionProps<Option, true>) {
|
||||||
function MultiValueLabel({
|
function MultiValueLabel({
|
||||||
data,
|
data,
|
||||||
...props
|
...props
|
||||||
}: MultiValueGenericProps<Option, true>) {
|
}: MultiValueGenericProps<EnvironmentAccess, true>) {
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<components.MultiValueLabel data={data} {...props}>
|
<components.MultiValueLabel data={data} {...props}>
|
||||||
|
@ -68,15 +70,15 @@ function MultiValueLabel({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Label({ option }: { option: Option }) {
|
function Label({ option }: { option: EnvironmentAccess }) {
|
||||||
const Icon = option.Type === 'user' ? UserIcon : TeamIcon;
|
const Icon = option.type === 'user' ? UserIcon : TeamIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Icon />
|
<Icon />
|
||||||
<span>{option.Name}</span>
|
<span>{option.name}</span>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<span>{option.Role.Name}</span>
|
<span>{option.role.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Layers } from 'lucide-react';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { WidgetTitle, WidgetBody, Widget } from '@@/Widget';
|
||||||
|
|
||||||
|
export function NamespaceDetailsWidget() {
|
||||||
|
const {
|
||||||
|
params: { id: namespaceName },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget aria-label="Namespace details">
|
||||||
|
<WidgetTitle icon={Layers} title="Namespace" />
|
||||||
|
<WidgetBody>
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>{namespaceName}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { ConfigMap } from 'kubernetes-types/core/v1';
|
||||||
|
import { concat, without } from 'lodash';
|
||||||
|
|
||||||
|
import { PortainerNamespaceAccessesConfigMap } from '@/react/kubernetes/configs/constants';
|
||||||
|
import { Configuration } from '@/react/kubernetes/configs/types';
|
||||||
|
|
||||||
|
import { NamespaceAccess } from './types';
|
||||||
|
|
||||||
|
export function createAuthorizeAccessConfigMapPayload(
|
||||||
|
namespaceAccesses: NamespaceAccess[],
|
||||||
|
selectedItems: NamespaceAccess[],
|
||||||
|
namespaceName: string,
|
||||||
|
configMap?: Configuration
|
||||||
|
): ConfigMap {
|
||||||
|
const newRemainingAccesses = concat(namespaceAccesses, ...selectedItems);
|
||||||
|
return createAccessConfigMapPayload(
|
||||||
|
newRemainingAccesses,
|
||||||
|
namespaceName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUnauthorizeAccessConfigMapPayload(
|
||||||
|
namespaceAccesses: NamespaceAccess[],
|
||||||
|
selectedItems: NamespaceAccess[],
|
||||||
|
namespaceName: string,
|
||||||
|
configMap?: Configuration
|
||||||
|
): ConfigMap {
|
||||||
|
const newRemainingAccesses = without(namespaceAccesses, ...selectedItems);
|
||||||
|
return createAccessConfigMapPayload(
|
||||||
|
newRemainingAccesses,
|
||||||
|
namespaceName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccessConfigMapPayload(
|
||||||
|
newRemainingAccesses: NamespaceAccess[],
|
||||||
|
namespaceName: string,
|
||||||
|
configMap?: Configuration
|
||||||
|
): ConfigMap {
|
||||||
|
const configMapAccessesValue = JSON.parse(
|
||||||
|
configMap?.Data?.[PortainerNamespaceAccessesConfigMap.accessKey] || '{}'
|
||||||
|
);
|
||||||
|
const newNamespaceAccesses = newRemainingAccesses.reduce(
|
||||||
|
(namespaceAccesses, accessItem) => {
|
||||||
|
if (accessItem.type === 'user') {
|
||||||
|
return {
|
||||||
|
...namespaceAccesses,
|
||||||
|
UserAccessPolicies: {
|
||||||
|
...namespaceAccesses.UserAccessPolicies,
|
||||||
|
// hardcode to 0, as they use their environment role
|
||||||
|
[`${accessItem.id}`]: { RoleId: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...namespaceAccesses,
|
||||||
|
TeamAccessPolicies: {
|
||||||
|
...namespaceAccesses.TeamAccessPolicies,
|
||||||
|
// hardcode to 0, as they use their environment role
|
||||||
|
[`${accessItem.id}`]: { RoleId: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UserAccessPolicies: {},
|
||||||
|
TeamAccessPolicies: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const newConfigMapAccessesValue = {
|
||||||
|
...configMapAccessesValue,
|
||||||
|
[namespaceName]: newNamespaceAccesses,
|
||||||
|
};
|
||||||
|
const updatedConfigMap: ConfigMap = {
|
||||||
|
metadata: {
|
||||||
|
name: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
|
namespace: PortainerNamespaceAccessesConfigMap.namespace,
|
||||||
|
uid: configMap?.UID,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...configMap?.Data,
|
||||||
|
[PortainerNamespaceAccessesConfigMap.accessKey]: JSON.stringify(
|
||||||
|
newConfigMapAccessesValue
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return updatedConfigMap;
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { PortainerNamespaceAccessesConfigMap } from '@/react/kubernetes/configs/constants';
|
||||||
|
import { User } from '@/portainer/users/types';
|
||||||
|
import { Team } from '@/react/portainer/users/teams/types';
|
||||||
|
import { Configuration } from '@/react/kubernetes/configs/types';
|
||||||
|
|
||||||
|
import { NamespaceAccess, NamespaceAccessesMap } from './types';
|
||||||
|
|
||||||
|
export function parseNamespaceAccesses(
|
||||||
|
data: Configuration | null,
|
||||||
|
namespaceName: string,
|
||||||
|
users: User[],
|
||||||
|
teams: Team[]
|
||||||
|
) {
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const namespacesAccesses: NamespaceAccessesMap = JSON.parse(
|
||||||
|
data?.Data?.[PortainerNamespaceAccessesConfigMap.accessKey] ?? '{}'
|
||||||
|
);
|
||||||
|
const userAccessesIds = Object.keys(
|
||||||
|
namespacesAccesses[namespaceName]?.UserAccessPolicies ?? {}
|
||||||
|
);
|
||||||
|
const userAccesses: NamespaceAccess[] = users
|
||||||
|
.filter((user) => userAccessesIds.includes(`${user.Id}`))
|
||||||
|
.map((user) => ({
|
||||||
|
id: user.Id,
|
||||||
|
name: user.Username,
|
||||||
|
type: 'user',
|
||||||
|
}));
|
||||||
|
const teamAccessesIds = Object.keys(
|
||||||
|
namespacesAccesses[namespaceName]?.TeamAccessPolicies ?? {}
|
||||||
|
);
|
||||||
|
const teamAccesses: NamespaceAccess[] = teams
|
||||||
|
.filter((team) => teamAccessesIds.includes(`${team.Id}`))
|
||||||
|
.map((team) => ({
|
||||||
|
id: team.Id,
|
||||||
|
name: team.Name,
|
||||||
|
type: 'team',
|
||||||
|
}));
|
||||||
|
return [...userAccesses, ...teamAccesses];
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
TeamAccessPolicies,
|
||||||
|
UserAccessPolicies,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
export type NamespaceAccess = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: 'user' | 'team';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnvironmentAccess = NamespaceAccess & {
|
||||||
|
role: { name: string; id: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NamespaceAccesses {
|
||||||
|
UserAccessPolicies?: UserAccessPolicies;
|
||||||
|
TeamAccessPolicies?: TeamAccessPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamespaceAccessesMap {
|
||||||
|
[key: string]: NamespaceAccesses;
|
||||||
|
}
|
|
@ -2,10 +2,9 @@ import { http, HttpResponse } from 'msw';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EnvironmentGroup,
|
|
||||||
EnvironmentGroupId,
|
EnvironmentGroupId,
|
||||||
} from '@/react/portainer/environments/environment-groups/types';
|
Environment,
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import { UserViewModel } from '@/portainer/models/user';
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
import { Tag } from '@/portainer/tags/types';
|
import { Tag } from '@/portainer/tags/types';
|
||||||
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
||||||
|
@ -13,6 +12,7 @@ import { server } from '@/setup-tests/server';
|
||||||
import { withTestRouter } from '@/react/test-utils/withRouter';
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||||
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
|
import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
|
||||||
|
|
||||||
import { EnvironmentItem } from './EnvironmentItem';
|
import { EnvironmentItem } from './EnvironmentItem';
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import {
|
||||||
EnvironmentStatus,
|
EnvironmentStatus,
|
||||||
PlatformType,
|
PlatformType,
|
||||||
EdgeTypes,
|
EdgeTypes,
|
||||||
|
EnvironmentGroupId,
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
|
||||||
import {
|
import {
|
||||||
refetchIfAnyOffline,
|
refetchIfAnyOffline,
|
||||||
useEnvironmentList,
|
useEnvironmentList,
|
||||||
|
|
|
@ -56,6 +56,7 @@ export function AccessDatatable({
|
||||||
isLoading={!dataset}
|
isLoading={!dataset}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
|
getRowId={(row) => `${row.Type}-${row.Id}`}
|
||||||
extendTableOptions={mergeOptions(
|
extendTableOptions={mergeOptions(
|
||||||
withMeta({
|
withMeta({
|
||||||
table: 'access-table',
|
table: 'access-table',
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { EnvironmentGroupId } from '../types';
|
||||||
|
|
||||||
import { buildUrl } from './queries/build-url';
|
import { buildUrl } from './queries/build-url';
|
||||||
import { EnvironmentGroup, EnvironmentGroupId } from './types';
|
import { EnvironmentGroup } from './types';
|
||||||
|
|
||||||
export async function getGroup(id: EnvironmentGroupId) {
|
export async function getGroup(id: EnvironmentGroupId) {
|
||||||
try {
|
try {
|
||||||
const { data: group } = await axios.get<EnvironmentGroup>(buildUrl(id));
|
const { data: group } = await axios.get<EnvironmentGroup>(buildUrl(id));
|
||||||
return group;
|
return group;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw parseAxiosError(e as Error, '');
|
throw parseAxiosError(e, 'Unable to retrieve group');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +19,6 @@ export async function getGroups() {
|
||||||
const { data: groups } = await axios.get<EnvironmentGroup[]>(buildUrl());
|
const { data: groups } = await axios.get<EnvironmentGroup[]>(buildUrl());
|
||||||
return groups;
|
return groups;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw parseAxiosError(e as Error, '');
|
throw parseAxiosError(e, 'Unable to retrieve groups');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { error as notifyError } from '@/portainer/services/notifications';
|
import { withGlobalError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
import { EnvironmentGroup, EnvironmentGroupId } from './types';
|
import { EnvironmentGroupId } from '../types';
|
||||||
|
|
||||||
|
import { EnvironmentGroup } from './types';
|
||||||
import { getGroup, getGroups } from './environment-groups.service';
|
import { getGroup, getGroups } from './environment-groups.service';
|
||||||
import { queryKeys } from './queries/query-keys';
|
import { queryKeys } from './queries/query-keys';
|
||||||
|
|
||||||
|
@ -17,16 +19,22 @@ export function useGroups<T = EnvironmentGroup[]>({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroup<T = EnvironmentGroup>(
|
export function useGroup<T = EnvironmentGroup>(
|
||||||
groupId: EnvironmentGroupId,
|
groupId?: EnvironmentGroupId,
|
||||||
select?: (group: EnvironmentGroup) => T
|
select?: (group: EnvironmentGroup | null) => T
|
||||||
) {
|
) {
|
||||||
const { data } = useQuery(queryKeys.group(groupId), () => getGroup(groupId), {
|
return useQuery(
|
||||||
|
queryKeys.group(groupId),
|
||||||
|
() => {
|
||||||
|
if (groupId === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getGroup(groupId);
|
||||||
|
},
|
||||||
|
{
|
||||||
staleTime: 50,
|
staleTime: 50,
|
||||||
select,
|
select,
|
||||||
onError(error) {
|
enabled: groupId !== undefined,
|
||||||
notifyError('Failed loading group', error as Error);
|
...withGlobalError('Failed loading group'),
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EnvironmentGroupId } from '../types';
|
import { EnvironmentGroupId } from '../../types';
|
||||||
|
|
||||||
export function buildUrl(id?: EnvironmentGroupId, action?: string) {
|
export function buildUrl(id?: EnvironmentGroupId, action?: string) {
|
||||||
let url = '/endpoint_groups';
|
let url = '/endpoint_groups';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { EnvironmentGroupId } from '../types';
|
import { EnvironmentGroupId } from '../../types';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
base: () => ['environment-groups'] as const,
|
base: () => ['environment-groups'] as const,
|
||||||
group: (id: EnvironmentGroupId) => [...queryKeys.base(), id] as const,
|
group: (id?: EnvironmentGroupId) => [...queryKeys.base(), id] as const,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
|
||||||
export type EnvironmentGroupId = number;
|
import {
|
||||||
|
TeamAccessPolicies,
|
||||||
|
UserAccessPolicies,
|
||||||
|
EnvironmentGroupId,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
export interface EnvironmentGroup {
|
export interface EnvironmentGroup {
|
||||||
// Environment(Endpoint) group Identifier
|
// Environment(Endpoint) group Identifier
|
||||||
|
@ -11,4 +15,6 @@ export interface EnvironmentGroup {
|
||||||
Description: string;
|
Description: string;
|
||||||
// List of tags associated to this environment(endpoint) group
|
// List of tags associated to this environment(endpoint) group
|
||||||
TagIds: TagId[];
|
TagIds: TagId[];
|
||||||
|
UserAccessPolicies?: UserAccessPolicies;
|
||||||
|
TeamAccessPolicies?: TeamAccessPolicies;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,14 @@ import axios, {
|
||||||
json2formData,
|
json2formData,
|
||||||
arrayToJson,
|
arrayToJson,
|
||||||
} from '@/portainer/services/axios';
|
} from '@/portainer/services/axios';
|
||||||
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
|
||||||
import { type TagId } from '@/portainer/tags/types';
|
|
||||||
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type EnvironmentGroupId,
|
||||||
type Environment,
|
type Environment,
|
||||||
ContainerEngine,
|
ContainerEngine,
|
||||||
EnvironmentCreationTypes,
|
EnvironmentCreationTypes,
|
||||||
} from '../types';
|
} from '@/react/portainer/environments/types';
|
||||||
|
import { type TagId } from '@/portainer/tags/types';
|
||||||
|
import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||||
|
|
||||||
import { buildUrl } from './utils';
|
import { buildUrl } from './utils';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import {
|
||||||
|
Environment,
|
||||||
|
EnvironmentId,
|
||||||
|
EnvironmentType,
|
||||||
|
EnvironmentSecuritySettings,
|
||||||
|
EnvironmentStatus,
|
||||||
|
EnvironmentGroupId,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
import { type TagId } from '@/portainer/tags/types';
|
import { type TagId } from '@/portainer/tags/types';
|
||||||
import { UserId } from '@/portainer/users/types';
|
import { UserId } from '@/portainer/users/types';
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||||
|
@ -9,13 +16,6 @@ import {
|
||||||
} from '@/react/edge/edge-stacks/types';
|
} from '@/react/edge/edge-stacks/types';
|
||||||
|
|
||||||
import { getPublicSettings } from '../../settings/settings.service';
|
import { getPublicSettings } from '../../settings/settings.service';
|
||||||
import type {
|
|
||||||
Environment,
|
|
||||||
EnvironmentId,
|
|
||||||
EnvironmentType,
|
|
||||||
EnvironmentSecuritySettings,
|
|
||||||
EnvironmentStatus,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
import { buildUrl } from './utils';
|
import { buildUrl } from './utils';
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import {
|
||||||
KubernetesSettings,
|
KubernetesSettings,
|
||||||
DeploymentOptions,
|
DeploymentOptions,
|
||||||
EndpointChangeWindow,
|
EndpointChangeWindow,
|
||||||
|
EnvironmentGroupId,
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
|
||||||
import { EnvironmentGroupId } from '../environment-groups/types';
|
|
||||||
import { buildUrl } from '../environment.service/utils';
|
import { buildUrl } from '../environment.service/utils';
|
||||||
|
|
||||||
import { environmentQueryKeys } from './query-keys';
|
import { environmentQueryKeys } from './query-keys';
|
||||||
|
|
|
@ -12,9 +12,8 @@ import { queryKeys as edgeGroupQueryKeys } from '@/react/edge/edge-groups/querie
|
||||||
import { queryKeys as groupQueryKeys } from '@/react/portainer/environments/environment-groups/queries/query-keys';
|
import { queryKeys as groupQueryKeys } from '@/react/portainer/environments/environment-groups/queries/query-keys';
|
||||||
import { tagKeys } from '@/portainer/tags/queries';
|
import { tagKeys } from '@/portainer/tags/queries';
|
||||||
|
|
||||||
import { EnvironmentId } from '../types';
|
import { EnvironmentId, EnvironmentGroupId } from '../types';
|
||||||
import { buildUrl } from '../environment.service/utils';
|
import { buildUrl } from '../environment.service/utils';
|
||||||
import { EnvironmentGroupId } from '../environment-groups/types';
|
|
||||||
|
|
||||||
import { environmentQueryKeys } from './query-keys';
|
import { environmentQueryKeys } from './query-keys';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import { TagId } from '@/portainer/tags/types';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
|
||||||
import { DockerSnapshot } from '@/react/docker/snapshots/types';
|
import { DockerSnapshot } from '@/react/docker/snapshots/types';
|
||||||
|
|
||||||
|
export type EnvironmentGroupId = number;
|
||||||
|
|
||||||
|
type RoleId = number;
|
||||||
|
interface AccessPolicy {
|
||||||
|
RoleId: RoleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserAccessPolicies = Record<number, AccessPolicy>; // map[UserID]AccessPolicy
|
||||||
|
export type TeamAccessPolicies = Record<number, AccessPolicy>;
|
||||||
|
|
||||||
export type EnvironmentId = number;
|
export type EnvironmentId = number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,6 +167,8 @@ export type Environment = {
|
||||||
* A message that describes the status. Should be included for Status Provisioning or Error.
|
* A message that describes the status. Should be included for Status Provisioning or Error.
|
||||||
*/
|
*/
|
||||||
StatusMessage?: EnvironmentStatusMessage;
|
StatusMessage?: EnvironmentStatusMessage;
|
||||||
|
UserAccessPolicies?: UserAccessPolicies;
|
||||||
|
TeamAccessPolicies?: TeamAccessPolicies;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
|
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
import { EnvironmentGroupId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Select } from '@@/form-components/Input';
|
import { Select } from '@@/form-components/Input';
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
|
||||||
import { UserId } from '@/portainer/users/types';
|
|
||||||
|
|
||||||
import { TLSConfiguration } from '../../settings/types';
|
import { TLSConfiguration } from '../../settings/types';
|
||||||
|
import {
|
||||||
|
TeamAccessPolicies,
|
||||||
|
UserAccessPolicies,
|
||||||
|
} from '../../environments/types';
|
||||||
|
|
||||||
export type Catalog = {
|
export type Catalog = {
|
||||||
repositories: string[];
|
repositories: string[];
|
||||||
|
@ -19,14 +20,6 @@ export enum RegistryTypes {
|
||||||
GITHUB,
|
GITHUB,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoleId = number;
|
|
||||||
interface AccessPolicy {
|
|
||||||
RoleId: RoleId;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserAccessPolicies = Record<UserId, AccessPolicy>; // map[UserID]AccessPolicy
|
|
||||||
type TeamAccessPolicies = Record<TeamId, AccessPolicy>;
|
|
||||||
|
|
||||||
export interface RegistryAccess {
|
export interface RegistryAccess {
|
||||||
UserAccessPolicies: UserAccessPolicies;
|
UserAccessPolicies: UserAccessPolicies;
|
||||||
TeamAccessPolicies: TeamAccessPolicies;
|
TeamAccessPolicies: TeamAccessPolicies;
|
||||||
|
|
|
@ -122,6 +122,7 @@ export function CreateTeamForm({ users, teams }: Props) {
|
||||||
isLoading={isSubmitting || addTeamMutation.isLoading}
|
isLoading={isSubmitting || addTeamMutation.isLoading}
|
||||||
loadingText="Creating team..."
|
loadingText="Creating team..."
|
||||||
icon={Plus}
|
icon={Plus}
|
||||||
|
className="!ml-0"
|
||||||
>
|
>
|
||||||
Create team
|
Create team
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
Loading…
Reference in New Issue