Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backport v2.8.next1] Add user retention admin interface #11314

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f1c7b02
Add new subheader slot to ResourceList/Masthead
rak-phillip Jun 3, 2024
e56493a
Add new user retention settings button
rak-phillip Jun 3, 2024
32b1c68
Stub out user retension settings form
rak-phillip Jun 4, 2024
aeede29
Fetch the user retention settings
rak-phillip Jun 5, 2024
f009fa1
Rename state variables and use entire object from settings
rak-phillip Jun 5, 2024
9a6216f
Save with CruResource
rak-phillip Jun 5, 2024
621002a
Reduce repetition with reactive
rak-phillip Jun 6, 2024
04603c4
Replace cru-resource with footer
rak-phillip Jun 6, 2024
f7b2764
Set max-width for inputs
rak-phillip Jun 6, 2024
5b84dd6
Move header into new component
rak-phillip Jun 6, 2024
f916999
Fix rendering of labeld input sub-labels
rak-phillip Jun 7, 2024
602ab90
Use block elements instead
rak-phillip Jun 7, 2024
94a1463
Fix spacing of toggle switch
rak-phillip Jun 7, 2024
e7cd9e4
Perform basic validation of user retention cron
rak-phillip Jun 7, 2024
c4802cb
Watch values and respond to changes in the form
rak-phillip Jun 7, 2024
73795cc
Add types for `Setting`
rak-phillip Jun 8, 2024
091d608
Add remaining missing types
rak-phillip Jun 8, 2024
fa950b1
Update return type of `useStore`
rak-phillip Jun 8, 2024
6866e5f
Route back and display success growl
rak-phillip Jun 10, 2024
a54f87b
Add an error message when saving fails
rak-phillip Jun 10, 2024
0262285
Fix typescript warnings
rak-phillip Jun 10, 2024
e806682
Add guards for user retention feature
rak-phillip Jun 10, 2024
61cfd77
Use `script setup`
rak-phillip Jun 10, 2024
120d3cc
Update the header name
rak-phillip Jun 10, 2024
27cd9e4
Use tooltips to clarify input usage
rak-phillip Jun 10, 2024
a020cfe
Update english translations for user retention form
rak-phillip Jun 10, 2024
b34e107
Add a unit tests
rak-phillip Jun 18, 2024
73efeba
Add e2e tests
rak-phillip Jun 19, 2024
e1568a4
Fix issues with user retention page
rak-phillip Jun 19, 2024
ab2c6b6
Remove console statement and comments
rak-phillip Jun 19, 2024
b7d177a
Revert "Add another tests"
rak-phillip Jun 20, 2024
9183256
Fix type warnings
rak-phillip Jun 25, 2024
f16027c
Revert typescript changes
rak-phillip Jun 26, 2024
c8d4f45
Enforce a truthy value for user last login
rak-phillip Jun 26, 2024
46cf6ba
Fix examples in translation strings
rak-phillip Jun 27, 2024
59258e0
Fix issue with typescript component imports
rak-phillip Jun 27, 2024
292ba4f
Add translations for the user retention growl
rak-phillip Jun 27, 2024
9a18958
Utilize built-in rules for cron errors
rak-phillip Jun 27, 2024
ec4a972
Backport TabTitle.vue
rak-phillip Jun 28, 2024
523f4b6
Backport missing e2e test function `expectToBeEnabled()`
rak-phillip Jun 28, 2024
2db7625
Backport missing e2e test function `expectToBeDisabled()`
rak-phillip Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cypress/e2e/po/components/async-button.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ export default class AsyncButtonPo extends ComponentPo {
return this.self().click({ force });
}

expectToBeDisabled(): Cypress.Chainable {
return this.self().should('have.attr', 'disabled', 'disabled');
}

expectToBeEnabled(): Cypress.Chainable {
return this.self().should('not.have.attr', 'disabled');
}

label(name: string): Cypress.Chainable {
return this.self().contains(name);
}
Expand Down
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 @@ -41,11 +41,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 @@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
import MgmtUsersListPo from '@/cypress/e2e/po/lists/management.cattle.io.user.po';
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 LinkPo from '@/cypress/e2e/po/components/link.po';

export default class UsersPo extends PagePo {
private static createPath(clusterId: string) {
Expand Down Expand Up @@ -31,4 +32,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());
}
}
90 changes: 90 additions & 0 deletions cypress/e2e/tests/pages/users-and-auth/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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(() => {
cy.login();
});

it('can redirect', () => {
const page = new PagePo('/c/local/auth');

page.goTo();

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 @@ -209,4 +209,4 @@
"follow-redirects": ">=1.14.7",
"merge": ">=2.1.1"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,20 @@ export default (
:hover="hoverTooltip"
:value="validationMessage"
/>
<label
v-if="cronHint"
class="cron-label"
>{{ cronHint }}</label>
<label
v-if="subLabel"
<div
v-if="cronHint || subLabel"
class="sub-label"
>{{ subLabel }}</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 @@ -5633,6 +5633,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 @@ -163,7 +163,7 @@ export default {
</script>

<template>
<header>
<header class="with-subheader">
<slot name="typeDescription">
<TypeDescription :resource="resource" />
</slot>
Expand All @@ -180,6 +180,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 @@ -221,4 +226,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>
Loading
Loading