Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions web/packages/teleport/src/Users/Users.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export const Loaded = () => {
);
};

export const UsersNotEqualMauNotice = () => {
return (
<MemoryRouter>
<Users {...sample} showMauInfo={true} />
</MemoryRouter>
);
};

export const Failed = () => {
const attempt = {
isProcessing: false,
Expand Down Expand Up @@ -139,4 +147,6 @@ const sample = {
InviteCollaborators: null,
onEmailPasswordResetClose: () => null,
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
};
55 changes: 54 additions & 1 deletion web/packages/teleport/src/Users/Users.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import React from 'react';
import { MemoryRouter } from 'react-router';
import { render, screen } from 'design/utils/testing';
import { render, screen, userEvent } from 'design/utils/testing';

import { ContextProvider } from 'teleport';
import { createTeleportContext } from 'teleport/mocks/contexts';
Expand Down Expand Up @@ -57,6 +57,8 @@ describe('invite collaborators integration', () => {
inviteCollaboratorsOpen: false,
onEmailPasswordResetClose: () => undefined,
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
};
});

Expand Down Expand Up @@ -105,6 +107,55 @@ describe('invite collaborators integration', () => {
});
});

test('Users not equal to MAU Notice', async () => {
const ctx = createTeleportContext();
let props: State;

props = {
attempt: {
message: 'success',
isSuccess: true,
isProcessing: false,
isFailed: false,
},
users: [],
fetchRoles: async () => [],
operation: { type: 'invite-collaborators' },
onStartCreate: () => undefined,
onStartDelete: () => undefined,
onStartEdit: () => undefined,
onStartReset: () => undefined,
onStartInviteCollaborators: () => undefined,
onClose: () => undefined,
onDelete: () => undefined,
onCreate: () => undefined,
onUpdate: () => undefined,
onReset: () => undefined,
onInviteCollaboratorsClose: () => undefined,
InviteCollaborators: null,
inviteCollaboratorsOpen: false,
onEmailPasswordResetClose: () => undefined,
EmailPasswordReset: null,
showMauInfo: true,
onDismissUsersMauNotice: jest.fn(),
};

const user = userEvent.setup();

render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...props} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.getByTestId('users-not-mau-alert')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Dismiss' }));
expect(props.onDismissUsersMauNotice).toHaveBeenCalled();
expect(screen.queryByTestId('users-not-mau-alert')).not.toBeInTheDocument();
});

describe('email password reset integration', () => {
const ctx = createTeleportContext();

Expand Down Expand Up @@ -139,6 +190,8 @@ describe('email password reset integration', () => {
inviteCollaboratorsOpen: false,
onEmailPasswordResetClose: () => undefined,
EmailPasswordReset: null,
showMauInfo: false,
onDismissUsersMauNotice: () => null,
};
});

Expand Down
38 changes: 37 additions & 1 deletion web/packages/teleport/src/Users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import React from 'react';
import { Indicator, Box, Alert, Button } from 'design';
import { Indicator, Box, Alert, Button, Link } from 'design';

import {
FeatureBox,
Expand Down Expand Up @@ -46,6 +46,8 @@ export function Users(props: State) {
onStartDelete,
onStartEdit,
onStartReset,
showMauInfo,
onDismissUsersMauNotice,
onClose,
onCreate,
onUpdate,
Expand Down Expand Up @@ -98,6 +100,40 @@ export function Users(props: State) {
<Indicator />
</Box>
)}
{showMauInfo && (
<Alert
data-testid="users-not-mau-alert"
dismissible
onDismiss={onDismissUsersMauNotice}
kind="info"
css={`
a.external-link {
color: ${({ theme }) => theme.colors.buttons.link.default};
}
`}
>
The users displayed here are not an accurate reflection of Monthly
Active Users (MAU). For example, users who log in through Single
Sign-On (SSO) providers such as Okta may only appear here temporarily
and disappear once their sessions expire. For more information, read
our documentation on{' '}
<Link
target="_blank"
href="https://goteleport.com/docs/usage-billing/#monthly-active-users"
className="external-link"
>
MAU
</Link>{' '}
and{' '}
<Link
href="https://goteleport.com/docs/reference/user-types/"
className="external-link"
>
User Types
</Link>
.
</Alert>
)}
{attempt.isFailed && <Alert kind="danger" children={attempt.message} />}
{attempt.isSuccess && (
<UserList
Expand Down
15 changes: 15 additions & 0 deletions web/packages/teleport/src/Users/useUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { useAttempt } from 'shared/hooks';

import { ExcludeUserField, User } from 'teleport/services/user';
import useTeleport from 'teleport/useTeleport';
import cfg from 'teleport/config';
import { storageService } from 'teleport/services/storageService';
import auth from 'teleport/services/auth/auth';

export default function useUsers({
Expand Down Expand Up @@ -119,10 +121,21 @@ export default function useUsers({
return items.map(r => r.name);
}

function onDismissUsersMauNotice() {
storageService.setUsersMAUAcknowledged();
}

useEffect(() => {
attemptActions.do(() => ctx.userService.fetchUsers().then(setUsers));
}, []);

// if the cluster has billing enabled, and usageBasedBilling, and they haven't acknowledged
// the info yet
const showMauInfo =
ctx.getFeatureFlags().billing &&
cfg.isUsageBasedBilling &&
!storageService.getUsersMauAcknowledged();

return {
attempt,
users,
Expand All @@ -143,6 +156,8 @@ export default function useUsers({
inviteCollaboratorsOpen,
onEmailPasswordResetClose,
EmailPasswordReset,
showMauInfo,
onDismissUsersMauNotice,
};
}

Expand Down
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, I think it'd be nice to have our code a little bit reorganized so that small features like this one are more self-contained. At the moment, it's one div that required changes in four different files.

The pattern with a big hook is one thing, but the boilerplate needed in storageService is something that I've run into in Connect as well. In #40900, I was close to adding another bunch of methods for managing a single state field just to handle VNet autostart. Instead, I added usePersistedState which can be used with any state field, without having to add methods for each field.

Arguably, with local storage in the Web UI the situation is more complex, as every value has to be transformed into a string (so there's no inherent type safety; in Connect, we at least have JSON to work with) and then there's KEEP_LOCALSTORAGE_KEYS_ON_LOGOUT.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it seems like storageService is overdue for some method that can handle all of the state and know which to keep/clear on logout and what not.

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const KEEP_LOCALSTORAGE_KEYS_ON_LOGOUT = [
KeysEnum.USER_PREFERENCES,
KeysEnum.RECOMMEND_FEATURE,
KeysEnum.LICENSE_ACKNOWLEDGED,
KeysEnum.USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED,
];

export const storageService = {
Expand Down Expand Up @@ -207,6 +208,21 @@ export const storageService = {
window.localStorage.setItem(KeysEnum.LICENSE_ACKNOWLEDGED, 'true');
},

getUsersMauAcknowledged(): boolean {
return (
window.localStorage.getItem(
KeysEnum.USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED
) === 'true'
);
},

setUsersMAUAcknowledged() {
window.localStorage.setItem(
KeysEnum.USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED,
'true'
);
},

broadcast(messageType, messageBody) {
window.localStorage.setItem(messageType, messageBody);
window.localStorage.removeItem(messageType);
Expand Down
3 changes: 2 additions & 1 deletion web/packages/teleport/src/services/storageService/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const KeysEnum = {
EXTERNAL_AUDIT_STORAGE_CTA_DISABLED:
'grv_teleport_external_audit_storage_disabled',
LICENSE_ACKNOWLEDGED: 'grv_teleport_license_acknowledged',

USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED:
'grv_users_not_equal_to_mau_acknowledged',
LOCAL_NOTIFICATION_STATES: 'grv_teleport_notification_states',
};

Expand Down