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

User traits editor UI #42620

Merged
merged 33 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
44925dd
editor mockup
flyinghermit Jun 6, 2024
67a92fc
add empty traits to available traits so user can select from dropdown
flyinghermit Jun 6, 2024
a103b2a
trait name and value editor
flyinghermit Jun 7, 2024
b9ae408
transform editor data to match with onSave func
flyinghermit Jun 7, 2024
e7616af
add new trait
flyinghermit Jun 7, 2024
aad4b5f
remove trait
flyinghermit Jun 7, 2024
ecec976
add available trait name list
flyinghermit Jun 7, 2024
30a133d
show traits header only when trait count is more than zero
flyinghermit Jun 7, 2024
715a04c
add duplicate trait rule, add requireAll wrapper to shared rules
flyinghermit Jun 10, 2024
8001bdd
update editor design
flyinghermit Jun 10, 2024
2a914e9
move TraitsEditor to separate file, add unit test
flyinghermit Jun 10, 2024
7872c76
Merge branch 'master' into sshah/user-traits-editor
flyinghermit Jun 10, 2024
6dde19b
fix edit story
flyinghermit Jun 10, 2024
58d6a79
update /users API to handle allTraits
flyinghermit Jun 10, 2024
b015d03
cleanup
flyinghermit Jun 10, 2024
ca0520e
prettier format
flyinghermit Jun 10, 2024
f5d24c8
move validation rule out from TraitsEditor component
flyinghermit Jun 10, 2024
5ac8f30
remove requiredAll rule wrapper
flyinghermit Jun 10, 2024
8965089
Merge branch 'master' into sshah/user-traits-editor
flyinghermit Jun 11, 2024
11c7330
Merge branch 'master' of github.com:gravitational/teleport into sshah…
flyinghermit Jun 11, 2024
0d6b589
rename trait vars and add api unit test:
flyinghermit Jun 11, 2024
2f13357
fix typo's, add js comments
flyinghermit Jun 11, 2024
1a39fd7
use pointer to traitsPreset so empty struct can be compared with nil
flyinghermit Jun 12, 2024
314783e
resovle review comments:
flyinghermit Jun 13, 2024
1a98872
remove describe
flyinghermit Jun 13, 2024
23e8524
use const for array, bring back commented empty string checker block
flyinghermit Jun 13, 2024
7d4ecd8
add maxWidth to DialogContent inner content to avoid flickering when …
flyinghermit Jun 13, 2024
c3b9777
resolve review comments
flyinghermit Jun 13, 2024
47df76a
filter empty element only if length equals 1. add unit test
flyinghermit Jun 13, 2024
1d38fb3
handlechange: return for empty string, trim whitespace
flyinghermit Jun 13, 2024
1e80fcf
explicitely handle null value
flyinghermit Jun 13, 2024
9a239ba
shorten empty string value check block
flyinghermit Jun 14, 2024
4c868e3
Merge branch 'master' into sshah/user-traits-editor
flyinghermit Jun 14, 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
15 changes: 11 additions & 4 deletions lib/web/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,13 @@ func updateUser(r *http.Request, m userAPIGetter) (*ui.User, error) {

user.SetRoles(req.Roles)

updateUserTraits(req, user)
// we do not want situation where one trait set (allTraits)
// overrides another (traits).
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
if len(req.AllTraits) > 0 {
user.SetTraits(req.AllTraits)
} else {
updateUserTraits(req, user)
}
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved

updated, err := m.UpdateUser(r.Context(), user)
if err != nil {
Expand Down Expand Up @@ -305,9 +311,10 @@ type userTraits struct {
// - if the value is an empty array we remove every element from the trait
// - otherwise, we replace the list for that trait
type saveUserRequest struct {
Name string `json:"name"`
Roles []string `json:"roles"`
Traits userTraits `json:"traits"`
Name string `json:"name"`
Roles []string `json:"roles"`
Traits userTraits `json:"traits"`
AllTraits map[string][]string `json:"allTraits"`
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
}

func (r *saveUserRequest) checkAndSetDefaults() error {
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
29 changes: 29 additions & 0 deletions web/packages/shared/components/Validation/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ const requiredEmailLike: Rule<string, EmailValidationResult> = email => () => {
};
};

/**
* A rule function that combines multiple inner rule functions. All rules must
* return `valid`, otherwise it returns a comma separated string containing all
* invalid rule messages.
* @param rules a list of rule functions to apply
* @returns a rule function that ANDs all input rules
*/
const requiredAll =
<T>(...rules: Rule<T | string | string[], ValidationResult>[]): Rule<T> =>
(value: T) =>
() => {
let messages = [];
for (let r of rules) {
let result = r(value)();
if (!result.valid) {
messages.push(result.message);
}
}

if (messages.length > 0) {
return {
valid: false,
message: messages.join('. '),
};
}
return { valid: true };
};

export {
requiredToken,
requiredPassword,
Expand All @@ -202,4 +230,5 @@ export {
requiredRoleArn,
requiredIamRoleName,
requiredEmailLike,
requiredAll,
};
92 changes: 92 additions & 0 deletions web/packages/teleport/src/Users/UserAddEdit/TraitsEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { fireEvent, render, screen } from 'design/utils/testing';

import Validation from 'shared/components/Validation';

import { AllUserTraits } from 'teleport/services/user';

import { TraitsEditor, traitsToTraitsOption, emptyTrait } from './TraitsEditor';

import type { TraitsOption } from './TraitsEditor';

describe('Render traits correctly', () => {
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
const userTraits = {
logins: ['root', 'ubuntu'],
db_roles: ['dbadmin', 'postgres'],
db_names: ['postgres', 'aurora'],
} as AllUserTraits;
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved

test('Avalable traits are rendered', async () => {
let configuredTraits = [] as TraitsOption[];

const setConfiguredTraits = jest.fn();

const { rerender } = render(
<TraitsEditor
allTraits={userTraits}
attempt={{ status: '' }}
configuredTraits={configuredTraits}
setConfiguredTraits={setConfiguredTraits}
/>
);

expect(screen.getByText('User Traits')).toBeInTheDocument();

expect(setConfiguredTraits).toHaveBeenLastCalledWith(
traitsToTraitsOption(userTraits)
);

rerender(
<Validation>
<TraitsEditor
allTraits={userTraits}
attempt={{ status: '' }}
configuredTraits={traitsToTraitsOption(userTraits)}
setConfiguredTraits={setConfiguredTraits}
/>
</Validation>
);
expect(screen.getAllByTestId('trait-key')).toHaveLength(3);
expect(screen.getAllByTestId('trait-value')).toHaveLength(3);
});

test('Add and remove Trait', async () => {
const userTraits = {} as AllUserTraits;

let configuredTraits = [] as TraitsOption[];

const setConfiguredTraits = jest.fn();

const { rerender } = render(
<Validation>
<TraitsEditor
allTraits={userTraits}
attempt={{ status: '' }}
configuredTraits={configuredTraits}
setConfiguredTraits={setConfiguredTraits}
/>
</Validation>
);
expect(screen.queryAllByTestId('trait-key')).toHaveLength(0);

const addButtonEl = screen.getByRole('button', { name: /Add user trait/i });
expect(addButtonEl).toBeInTheDocument();
fireEvent.click(addButtonEl);

expect(setConfiguredTraits).toHaveBeenLastCalledWith([emptyTrait]);

const singleTrait = { logins: ['root', 'ubuntu'] };
rerender(
<Validation>
<TraitsEditor
allTraits={singleTrait}
attempt={{ status: '' }}
configuredTraits={traitsToTraitsOption(singleTrait)}
setConfiguredTraits={setConfiguredTraits}
/>
</Validation>
);
fireEvent.click(screen.getByTitle('Remove Trait'));
expect(setConfiguredTraits).toHaveBeenCalledWith([]);
flyinghermit marked this conversation as resolved.
Show resolved Hide resolved
});
});
Loading
Loading