Skip to content

Commit

Permalink
Merge pull request rancher#11276 from rak-phillip/feature/user-retent…
Browse files Browse the repository at this point in the history
…ion-settings

Add user retention admin interface
  • Loading branch information
rak-phillip authored Jun 27, 2024
2 parents 23bc22f + 026097a commit a1b5f14
Show file tree
Hide file tree
Showing 18 changed files with 695 additions and 19 deletions.
8 changes: 3 additions & 5 deletions cypress/e2e/po/components/labeled-input.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,9 @@ export default class LabeledInputPo extends ComponentPo {
}

value(): Cypress.Chainable {
throw new Error('Not implements');
// The text for the input field is in a shadow dom element. Neither the proposed two methods
// to dive in to the shadow dom work
// return this.input().find('div', { includeShadowDom: true }).invoke('text');
// return this.input().shadow().find('div').invoke('text');
return this.input().then(($element) => {
return $element.prop('value');
});
}

/**
Expand Down
7 changes: 7 additions & 0 deletions cypress/e2e/po/components/link.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ComponentPo from '@/cypress/e2e/po/components/component.po';

export default class LinkPo extends ComponentPo {
click(force = false): Cypress.Chainable {
return this.self().click({ force });
}
}
52 changes: 52 additions & 0 deletions cypress/e2e/po/pages/users-and-auth/user.retention.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

import PagePo from '@/cypress/e2e/po/pages/page.po';
import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po';
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
import ToggleSwitchPo from '@/cypress/e2e/po/components/toggle-switch.po';

export default class UserRetentionPo extends PagePo {
constructor(private clusterId = '_') {
super(UserRetentionPo.createPath(clusterId));
}

private static createPath(clusterId: string) {
return `/c/${ clusterId }/auth/user.retention`;
}

saveButton() {
return new AsyncButtonPo('[data-testid="action-button-async-button"]');
}

enableRegistryCheckbox(): CheckboxInputPo {
return new CheckboxInputPo('[data-testid="registries-enable-checkbox"]');
}

disableAfterPeriodCheckbox(): CheckboxInputPo {
return new CheckboxInputPo('[data-testid="disableAfterPeriod"]');
}

disableAfterPeriodInput() {
return new LabeledInputPo('[data-testid="disableAfterPeriodInput"]');
}

deleteAfterPeriodCheckbox(): CheckboxInputPo {
return new CheckboxInputPo('[data-testid="deleteAfterPeriod"]');
}

deleteAfterPeriodInput() {
return new LabeledInputPo('[data-testid="deleteAfterPeriodInput"]');
}

userRetentionCron() {
return new LabeledInputPo('[data-testid="userRetentionCron"]');
}

userRetentionDryRun() {
return new ToggleSwitchPo('[data-testid="userRetentionDryRun"]');
}

userLastLoginDefault() {
return new LabeledInputPo('[data-testid="userLastLoginDefault"]');
}
}
5 changes: 5 additions & 0 deletions cypress/e2e/po/pages/users-and-auth/users.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MgmtUserEditPo from '@/cypress/e2e/po/edit/management.cattle.io.user.po';
import MgmtUserResourceDetailPo from '@/cypress/e2e/po/detail/management.cattle.io.user.po';
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
import LinkPo from '@/cypress/e2e/po/components/link.po';

export default class UsersPo extends PagePo {
private static createPath(clusterId: string) {
Expand Down Expand Up @@ -40,4 +41,8 @@ export default class UsersPo extends PagePo {
detail(userId: string) {
return new MgmtUserResourceDetailPo(this.clusterId, userId);
}

userRetentionLink() {
return new LinkPo('[data-testid="router-link-user-retention"]', this.self());
}
}
75 changes: 75 additions & 0 deletions cypress/e2e/tests/pages/users-and-auth/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import PagePo from '@/cypress/e2e/po/pages/page.po';
import UsersPo from '@/cypress/e2e/po/pages/users-and-auth/users.po';
import UserRetentionPo from '@/cypress/e2e/po/pages/users-and-auth/user.retention.po';

describe('Auth Index', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
before(() => {
Expand All @@ -12,4 +14,77 @@ describe('Auth Index', { testIsolation: 'off', tags: ['@explorer', '@adminUser']

cy.url().should('includes', `${ Cypress.config().baseUrl }/c/local/auth/management.cattle.io.user`);
});

it('can navigate to user retention settings', () => {
const page = new PagePo('/c/local/auth');

page.goTo();

const usersPo = new UsersPo();

usersPo.userRetentionLink().click();

cy.url().should('includes', `${ Cypress.config().baseUrl }/c/local/auth/user.retention`);
});

it('save button should be disabled when form is invalid', () => {
const page = new PagePo('/c/local/auth/user.retention');

page.goTo();

const userRetentionPo = new UserRetentionPo();

userRetentionPo.disableAfterPeriodCheckbox().set();
userRetentionPo.disableAfterPeriodInput().set('30d');

userRetentionPo.saveButton().expectToBeDisabled();
});

it('save button should be enabled when form is valid', () => {
const page = new PagePo('/c/local/auth/user.retention');

page.goTo();

const userRetentionPo = new UserRetentionPo();

userRetentionPo.disableAfterPeriodCheckbox().set();
userRetentionPo.disableAfterPeriodInput().set('30d');
userRetentionPo.userRetentionCron().set('0 0 1 1 *');

userRetentionPo.saveButton().expectToBeEnabled();
});

it('can save user retention settings', () => {
const page = new PagePo('/c/local/auth/user.retention');

page.goTo();

const userRetentionPo = new UserRetentionPo();

userRetentionPo.disableAfterPeriodCheckbox().set();
userRetentionPo.disableAfterPeriodInput().set('300h');
userRetentionPo.deleteAfterPeriodCheckbox().set();
userRetentionPo.deleteAfterPeriodInput().set('600h');
userRetentionPo.userRetentionCron().set('0 0 1 1 *');
// userRetentionPo.userRetentionDryRun().set('true');
userRetentionPo.userLastLoginDefault().set('1718744536000');

userRetentionPo.saveButton().expectToBeEnabled();
userRetentionPo.saveButton().click();

cy.url().should('include', '/management.cattle.io.user');

const usersPo = new UsersPo();

usersPo.userRetentionLink().checkVisible();
usersPo.userRetentionLink().click();

userRetentionPo.disableAfterPeriodCheckbox().checkExists();
userRetentionPo.disableAfterPeriodCheckbox().isChecked();
userRetentionPo.disableAfterPeriodInput().value().should('equal', '300h');
userRetentionPo.deleteAfterPeriodCheckbox().isChecked();
userRetentionPo.deleteAfterPeriodInput().value().should('equal', '600h');
userRetentionPo.userRetentionCron().value().should('equal', '0 0 1 1 *');
userRetentionPo.userLastLoginDefault().value().should('equal', '1718744536000');
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,4 @@
"merge": ">=2.1.1",
"semver": ">=7.5.2"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,20 @@ export default defineComponent({
:hover="hoverTooltip"
:value="validationMessage"
/>
<label
v-if="cronHint"
class="cron-label"
>{{ cronHint }}</label>
<label
v-if="subLabel"
v-clean-html="subLabel"
<div
v-if="cronHint || subLabel"
class="sub-label"
/>
>
<div
v-if="cronHint"
>
{{ cronHint }}
</div>
<div
v-if="subLabel"
v-clean-html="subLabel"
/>
</div>
</div>
</template>
<style scoped lang="scss">
Expand Down
1 change: 1 addition & 0 deletions shell/assets/styles/global/_labeled-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
.cron-label, .sub-label {
position: absolute;
top: 100%;
width: 100%;
padding-top: 5px;
left: 0;
color: var(--input-label);
Expand Down
35 changes: 35 additions & 0 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5800,6 +5800,41 @@ user:
placeholder: e.g. This account is for John Smith
list:
errorRefreshingGroupMemberships: Error refreshing group memberships
retention:
button:
label: User retention settings
growl:
title: Save user retention settings
message: User retention settings have been updated successfully
edit:
title:
header: "Users: Settings"
pre: "Users:"
post: Settings
subTitle: User retention
form:
disableAfter:
checkbox: Disable user accounts after an inactivity period (duration since last login)
input:
label: Inactivity period
tooltip: Uses duration units (e.g. use 30h for 30 hours)
deleteAfter:
checkbox: Delete user accounts after an inactivity period (duration since last login)
input:
label: Inactivity period
tooltip: Uses duration units (e.g. use 30h for 30 hours)
subLabel: This value must be larger than the Disable period, if it's active
cron:
label: User retention process schedule
subLabel: The user retention process runs as a cron job (required)
errorMessage: User retention process schedule must be a valid cron expression
dryRun:
label: Run the user retention process in DRY mode (no changes will be applied)
subLabel: You can check the logs to see which accounts would be affected
defaultLastLogin:
label: Default last login (ms)
subLabel: Accounts without a registered last login timestamp will get this as a default
placeholder: Unix timestamp
validation:
noUpperCase: 'Alphanumeric characters in "{key}" must be lowercase'
arrayLength:
Expand Down
19 changes: 18 additions & 1 deletion shell/components/ResourceList/Masthead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export default {
</script>
<template>
<header>
<header class="with-subheader">
<slot name="typeDescription">
<TypeDescription :resource="resource" />
</slot>
Expand All @@ -181,6 +181,11 @@ export default {
:indeterminate="loadIndeterminate"
/>
</div>
<div class="sub-header">
<slot name="subHeader">
<!--Slot content-->
</slot>
</div>
<div class="actions-container">
<slot name="actions">
<div class="actions">
Expand Down Expand Up @@ -222,4 +227,16 @@ export default {
header {
margin-bottom: 20px;
}
header.with-subheader {
grid-template-areas:
'type-banner type-banner'
'title actions'
'sub-header sub-header'
'state-banner state-banner';
}
.sub-header {
grid-area: sub-header;
}
</style>
34 changes: 34 additions & 0 deletions shell/components/user.retention/user-retention-header.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import TabTitle from '@shell/components/TabTitle';
</script>

<template>
<div>
<header>
<div class="title">
<h1
data-testid="charts-header-title"
class="m-0"
>
<TabTitle :show-child="false">
{{ t('user.retention.edit.title.header') }}
</TabTitle>
<router-link
:to="{
name: 'c-cluster-product-resource',
params: {
cluster: '_',
product: 'auth',
resource: 'management.cattle.io.user',
}
}"
>
{{ t('user.retention.edit.title.pre') }}
</router-link>
{{ t('user.retention.edit.title.post') }}
</h1>
</div>
</header>
<h2>{{ t('user.retention.edit.title.subTitle') }}</h2>
</div>
</template>
26 changes: 26 additions & 0 deletions shell/composables/useI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Store } from 'vuex';

import { stringFor } from '@shell/plugins/i18n';

let store: Store<any> | null = null;

export const useI18n = (vuexStore: Store<any>): { t: typeof t } => {
store = vuexStore;

if (!store) {
throw new Error('usI18n() must be called from setup()');
}

return { t };
};

/**
* Allows for consuming i18n strings with the Vue composition API.
* @param key - The key for the i18n string to translate.
* @param args - An object or array containing arguments for the translation function.
* @param raw - A boolean determining if the string returned is a raw representation.
* @returns A translated string or the raw value if the raw parameter is set to true.
*/
const t = (key: string, args?: unknown, raw?: boolean): unknown => {
return stringFor(store, key, args, raw);
};
3 changes: 2 additions & 1 deletion shell/composables/useStore.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getCurrentInstance } from 'vue';
import { Store } from 'vuex';

/**
* useStore allows for accessing Vuex stores from within a setup function. This is a temporary measure for working with
* Vuex in Vue2.
*
* TODO: #9541 Remove for Vue 3 migration
*/
export const useStore = ():unknown => {
export const useStore = ():Store<any> => {
const vm = getCurrentInstance();

if (!vm) throw new Error('useStore() must be called from setup()');
Expand Down
4 changes: 4 additions & 0 deletions shell/config/router/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ export default [
path: '/c/:cluster/auth/group.principal/assign-edit',
component: () => interopDefault(import('@shell/pages/c/_cluster/auth/group.principal/assign-edit.vue')),
name: 'c-cluster-auth-group.principal-assign-edit'
}, {
path: '/c/:cluster/auth/user.retention',
component: () => interopDefault(import('@shell/pages/c/_cluster/auth/user.retention/index.vue')),
name: 'c-cluster-auth-user.retention'
}, {
path: '/c/:cluster/legacy/project/pipelines',
component: () => interopDefault(import('@shell/pages/c/_cluster/legacy/project/pipelines.vue')),
Expand Down
Loading

0 comments on commit a1b5f14

Please sign in to comment.