diff --git a/assets/translations/en-us.yaml b/assets/translations/en-us.yaml
index 3ee16453cb..2b3ae7fc49 100644
--- a/assets/translations/en-us.yaml
+++ b/assets/translations/en-us.yaml
@@ -1686,6 +1686,33 @@ login:
loginWithLocal: Log in with Local User
useProvider: Use a {provider} user
+members:
+ clusterMembers: Cluster Members
+ createActionLabel: Add
+ clusterPermissions:
+ noDescription: User created - no description
+ label: Cluster Permissions
+ description: Controls what access users have to the Cluster
+ createProjects: Create Projects
+ manageClusterBackups: Manage Cluster Backups
+ manageClusterCatalogs: Manage Cluster Catalogs
+ manageClusterMembers: Manage Cluster Members
+ manageNodes: Manage Nodes
+ manageStorage: Manage Storage
+ viewAllProjects: View All Projects
+ viewClusterCatalogs: View Cluster Catalogs
+ viewClusterMembers: View Cluster Members
+ viewNodes: View Nodes
+ owner:
+ label: Owner
+ description: Owners have full control over the Cluster and all resources inside it.
+ member:
+ label: Member
+ description: Members can manage the resources inside the Cluster but not change the Cluster itself.
+ custom:
+ label: Custom
+ description: Choose individual roles for this user.
+
monitoring:
accessModes:
many: ReadWriteMany
@@ -2500,6 +2527,8 @@ rbac:
fleetworkspace-admin: Admin
fleetworkspace-member: Member
fleetworkspace-readonly: Read-Only
+ members:
+ label: Members
roletemplate:
label: Roles
newUserDefault:
@@ -4129,6 +4158,11 @@ typeLabel:
one { Cluster Group }
other {Cluster Groups }
}
+ management.cattle.io.clusterroletemplatebinding: |-
+ {count, plural,
+ one { Cluster Member }
+ other { Cluster Members }
+ }
fleet.cattle.io.gitrepo: |-
{count, plural,
one { Git Repo }
@@ -4215,6 +4249,11 @@ typeLabel:
one { User }
other { Users }
}
+ namespace: |-
+ {count, plural,
+ one { Namespace }
+ other { Namespaces }
+ }
group.principal: |-
{count, plural,
one { Group }
diff --git a/components/ResourceDetail/Masthead.vue b/components/ResourceDetail/Masthead.vue
index 47e4431a21..5470ab48ed 100644
--- a/components/ResourceDetail/Masthead.vue
+++ b/components/ResourceDetail/Masthead.vue
@@ -317,7 +317,8 @@ export default {
{{ parent.displayName }}:
{{ parent.displayName }}:
-
+ {{ value.detailPageHeaderActionOverride(realMode) }}
+
diff --git a/components/form/RadioButton.vue b/components/form/RadioButton.vue
index 274bd2fc14..1d511e63a1 100644
--- a/components/form/RadioButton.vue
+++ b/components/form/RadioButton.vue
@@ -34,7 +34,17 @@ export default {
mode: {
type: String,
default: 'edit',
- }
+ },
+
+ descriptionKey: {
+ type: String,
+ default: null
+ },
+
+ description: {
+ type: String,
+ default: null
+ },
},
data() {
@@ -102,17 +112,27 @@ export default {
:aria-checked="isChecked"
role="radio"
/>
-
+
+
+
+
+
+ {{ description }}
+
+
+
diff --git a/components/form/RadioGroup.vue b/components/form/RadioGroup.vue
index 3adc7118d5..7b9936aabd 100644
--- a/components/form/RadioGroup.vue
+++ b/components/form/RadioGroup.vue
@@ -150,6 +150,7 @@ export default {
:name="name"
:value="value"
:label="option.label"
+ :description="option.description"
:val="option.value"
:disabled="isDisabled"
:mode="mode"
diff --git a/config/product/explorer.js b/config/product/explorer.js
index a486716bce..1654276a69 100644
--- a/config/product/explorer.js
+++ b/config/product/explorer.js
@@ -4,6 +4,7 @@ import {
WORKLOAD, WORKLOAD_TYPES, SERVICE, HPA, NETWORK_POLICY, PV, PVC, STORAGE_CLASS, POD,
RBAC,
MANAGEMENT,
+ NAMESPACE,
NORMAN,
} from '@/config/types';
@@ -27,6 +28,7 @@ export function init(store) {
basicType,
ignoreType,
mapGroup,
+ mapType,
weightGroup,
weightType,
headers,
@@ -40,7 +42,7 @@ export function init(store) {
weight: 3,
showNamespaceFilter: true,
icon: 'compass',
- typeStoreMap: { [MANAGEMENT.PROJECT]: 'management' }
+ typeStoreMap: { [MANAGEMENT.PROJECT]: 'management', [MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING]: 'management' }
});
basicType(['cluster-dashboard', 'cluster-tools']);
@@ -77,6 +79,7 @@ export function init(store) {
RBAC.CLUSTER_ROLE,
RBAC.ROLE_BINDING,
RBAC.CLUSTER_ROLE_BINDING,
+ 'cluster-members'
], 'rbac');
weightGroup('cluster', 99, true);
@@ -124,9 +127,12 @@ export function init(store) {
mapGroup(/^(.*\.)?cluster\.x-k8s\.io$/, 'Cluster Provisioning');
mapGroup(/^(aks|eks|gke|rke|rke-machine-config|provisioning)\.cattle\.io$/, 'Cluster Provisioning');
+ mapType(MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING, store.getters['i18n/t'](`typeLabel.${ MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING }`, { count: 2 }));
+
configureType(NODE, { isCreatable: false, isEditable: false });
configureType(WORKLOAD_TYPES.JOB, { isEditable: false, match: WORKLOAD_TYPES.JOB });
configureType(PVC, { isEditable: false });
+ configureType(MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING, { isEditable: false });
configureType('workload', {
displayName: 'Workload',
@@ -200,7 +206,7 @@ export function init(store) {
]);
virtualType({
- label: 'Cluster Dashboard',
+ label: store.getters['i18n/t']('clusterIndexPage.header'),
group: 'Root',
namespaced: false,
name: 'cluster-dashboard',
@@ -211,7 +217,18 @@ export function init(store) {
});
virtualType({
- label: 'Overview',
+ label: store.getters['i18n/t']('members.clusterMembers'),
+ group: 'rbac',
+ namespaced: false,
+ name: 'cluster-members',
+ icon: 'globe',
+ weight: 100,
+ route: { name: 'c-cluster-explorer-members' },
+ exact: true,
+ });
+
+ virtualType({
+ label: store.getters['i18n/t']('generic.overview'),
group: 'Workload',
namespaced: true,
name: 'workload',
@@ -226,7 +243,7 @@ export function init(store) {
});
virtualType({
- label: 'Projects/Namespaces',
+ label: store.getters['i18n/t']('projectNamespaces.label'),
group: 'cluster',
icon: 'globe',
namespaced: false,
@@ -238,7 +255,7 @@ export function init(store) {
});
virtualType({
- label: 'Namespaces',
+ label: store.getters['i18n/t'](`typeLabel.${ NAMESPACE }`, { count: 2 }),
group: 'cluster',
icon: 'globe',
namespaced: false,
diff --git a/config/table-headers.js b/config/table-headers.js
index b8aff11700..a50873de71 100644
--- a/config/table-headers.js
+++ b/config/table-headers.js
@@ -177,6 +177,14 @@ export const RAM = {
width: 120,
};
+export const PRINCIPAL = {
+ name: 'principal',
+ labelKey: 'tableHeaders.name',
+ sort: 'principal.loginName',
+ value: 'userPrincipalName',
+ formatter: 'Principal',
+};
+
export const PODS = {
name: 'pods',
labelKey: 'tableHeaders.pods',
@@ -789,6 +797,12 @@ export const RESTART = {
width: 75,
};
+export const ROLE = {
+ name: 'role',
+ value: 'roleDisplay',
+ labelKey: 'tableHeaders.role',
+};
+
export const FEATURE_DESCRIPTION = {
name: 'description',
labelKey: 'tableHeaders.description',
diff --git a/config/types.js b/config/types.js
index 955b438db7..e75ceb4c0e 100644
--- a/config/types.js
+++ b/config/types.js
@@ -12,14 +12,15 @@ export const STEVE = {
// Auth (via Norman)
// Base: /v3
export const NORMAN = {
- AUTH_CONFIG: 'authconfig',
- ETCD_BACKUP: 'etcdbackup',
- CLUSTER_TOKEN: 'clusterregistrationtoken',
- GROUP: 'group',
- PRINCIPAL: 'principal',
- SPOOFED: { GROUP_PRINCIPAL: 'group.principal' },
- TOKEN: 'token',
- USER: 'user',
+ AUTH_CONFIG: 'authconfig',
+ ETCD_BACKUP: 'etcdbackup',
+ CLUSTER_TOKEN: 'clusterregistrationtoken',
+ CLUSTER_ROLE_TEMPLATE_BINDING: 'clusterRoleTemplateBinding',
+ GROUP: 'group',
+ PRINCIPAL: 'principal',
+ SPOOFED: { GROUP_PRINCIPAL: 'group.principal' },
+ TOKEN: 'token',
+ USER: 'user',
};
// Public (via Norman)
diff --git a/edit/management.cattle.io.clusterroletemplatebinding.vue b/edit/management.cattle.io.clusterroletemplatebinding.vue
new file mode 100644
index 0000000000..462f5daaac
--- /dev/null
+++ b/edit/management.cattle.io.clusterroletemplatebinding.vue
@@ -0,0 +1,222 @@
+
+
+
+
+ errors = e"
+ @finish="saveOverride"
+ @cancel="done"
+ >
+
+
+
+
+
+
{{ t('members.clusterPermissions.label') }}
+
+ {{ t('members.clusterPermissions.description') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/models/management.cattle.io.clusterroletemplatebinding.js b/models/management.cattle.io.clusterroletemplatebinding.js
index 55744e350b..4a47e28ac1 100644
--- a/models/management.cattle.io.clusterroletemplatebinding.js
+++ b/models/management.cattle.io.clusterroletemplatebinding.js
@@ -1,6 +1,41 @@
+import { _CREATE } from '@/config/query-params';
import { MANAGEMENT } from '@/config/types';
export default {
+ detailPageHeaderActionOverride() {
+ return (realMode) => {
+ if (realMode === _CREATE) {
+ return this.t('members.createActionLabel');
+ }
+ };
+ },
+ canCustomEdit() {
+ return false;
+ },
+
+ canYaml() {
+ return false;
+ },
+
+ canClone() {
+ return false;
+ },
+
+ user() {
+ return this.$rootGetters['management/byId'](MANAGEMENT.USER, this.userName);
+ },
+
+ nameDisplay() {
+ return this.user?.nameDisplay;
+ },
+
+ roleDisplay() {
+ return this.roleTemplate.nameDisplay;
+ },
+
+ roleDescription() {
+ return this.roleTemplate.description;
+ },
roleTemplate() {
return this.$rootGetters['management/byId'](MANAGEMENT.ROLE_TEMPLATE, this.roleTemplateName);
@@ -29,4 +64,20 @@ export default {
return { name, params };
},
+
+ listLocation() {
+ return { name: 'c-cluster-explorer-members' };
+ },
+
+ doneOverride() {
+ return this.listLocation;
+ },
+
+ parentLocationOverride() {
+ return this.listLocation;
+ },
+
+ subSearch() {
+ return [{ nameDisplay: this.nameDisplay }];
+ }
};
diff --git a/models/management.cattle.io.user.js b/models/management.cattle.io.user.js
index cc7f064b17..9ab6b361d0 100644
--- a/models/management.cattle.io.user.js
+++ b/models/management.cattle.io.user.js
@@ -17,6 +17,12 @@ export default {
return !!(this.principalIds || []).find(p => p === currentPrincipal);
},
+ principals() {
+ return this.principalIds
+ .map(id => this.$rootGetters['rancher/byId'](NORMAN.PRINCIPAL, id))
+ .filter(p => p);
+ },
+
nameDisplay() {
return this.displayName || this.username || this.id;
},
diff --git a/models/namespace.js b/models/namespace.js
index 2fd63ccf16..86486427af 100644
--- a/models/namespace.js
+++ b/models/namespace.js
@@ -113,7 +113,7 @@ export default {
},
projectNameSort() {
- return this.project?.nameSort || '}';
+ return this.project?.nameSort || '';
},
istioInstalled() {
diff --git a/pages/c/_cluster/explorer/members/index.vue b/pages/c/_cluster/explorer/members/index.vue
new file mode 100644
index 0000000000..ae6c3d157c
--- /dev/null
+++ b/pages/c/_cluster/explorer/members/index.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
diff --git a/plugins/steve/resource-instance.js b/plugins/steve/resource-instance.js
index 8fe0ffc10a..ec3c38818f 100644
--- a/plugins/steve/resource-instance.js
+++ b/plugins/steve/resource-instance.js
@@ -684,7 +684,7 @@ export default {
action: (this.canCustomEdit ? 'goToClone' : 'cloneYaml'),
label: this.t('action.clone'),
icon: 'icon icon-copy',
- enabled: this.canCreate && (this.canCustomEdit || this.canYaml),
+ enabled: this.canClone && this.canCreate && (this.canCustomEdit || this.canYaml),
},
{ divider: true },
{
@@ -717,6 +717,10 @@ export default {
return this.hasLink('remove') && this.$rootGetters['type-map/optionsFor'](this.type).isRemovable;
},
+ canClone() {
+ return true;
+ },
+
canUpdate() {
return this.hasLink('update') && this.$rootGetters['type-map/optionsFor'](this.type).isEditable;
},
@@ -839,7 +843,7 @@ export default {
const schema = this.$getters['schemaFor'](this.type);
let url = schema.linkFor('collection');
- if ( schema.attributes && schema.attributes.namespaced ) {
+ if ( schema.attributes && schema.attributes.namespaced && this.metadata && this.metadata.namespace ) {
url += `/${ this.metadata.namespace }`;
}