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" /> - +
+ +
+ + +
+
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 @@ + + + + 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 }`; }