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,
};
68 changes: 67 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,68 @@ describe('invite collaborators integration', () => {
});
});

test('Users not equal to MAU Notice', async () => {
const user = userEvent.setup();
const ctx = createTeleportContext();

const props: State = {
attempt: {
message: 'success',
isSuccess: true,
isProcessing: false,
isFailed: false,
},
users: [],
fetchRoles: () => Promise.resolve([]),
operation: {
type: 'reset',
user: { name: 'alice@example.com', roles: ['foo'] },
},

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 { rerender } = render(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...props} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.getByTestId('users-not-mau-alert')).toBeInTheDocument();
await user.click(screen.getByTestId('dismiss-users-not-mau-alert'));
expect(props.onDismissUsersMauNotice).toHaveBeenCalled();

const newProps = { ...props, showMauInfo: false };

rerender(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<Users {...newProps} />
</ContextProvider>
</MemoryRouter>
);

expect(screen.queryByTestId('users-not-mau-alert')).not.toBeInTheDocument();
});

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

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

Expand Down
33 changes: 32 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,8 @@
*/

import React from 'react';
import { Indicator, Box, ButtonPrimary, Alert } from 'design';
import { Indicator, Box, Alert, ButtonPrimary, Link, ButtonIcon } from 'design';
import { Cross } from 'design/Icon';

import {
FeatureBox,
Expand Down Expand Up @@ -46,6 +47,8 @@ export function Users(props: State) {
onStartDelete,
onStartEdit,
onStartReset,
showMauInfo,
onDismissUsersMauNotice,
onClose,
onCreate,
onUpdate,
Expand Down Expand Up @@ -86,6 +89,34 @@ export function Users(props: State) {
<Indicator />
</Box>
)}
{showMauInfo && (
<Alert data-testid="users-not-mau-alert" kind="info">
<Box>
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"
>
MAU
</Link>{' '}
and{' '}
<Link href="https://goteleport.com/docs/reference/user-types/">
User Types
</Link>
.
</Box>
<ButtonIcon
data-testid="dismiss-users-not-mau-alert"
onClick={onDismissUsersMauNotice}
>
<Cross />
</ButtonIcon>
</Alert>
)}
{attempt.isFailed && <Alert kind="danger" children={attempt.message} />}
{attempt.isSuccess && (
<UserList
Expand Down
16 changes: 16 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 All @@ -30,6 +32,13 @@ export default function useUsers({
const ctx = useTeleport();
const [attempt, attemptActions] = useAttempt({ isProcessing: true });
const [users, setUsers] = useState([] as User[]);
// if the cluster has billing enabled, and usageBasedBilling, and they haven't acknowledged
// the info yet
const [showMauInfo, setShowMauInfo] = useState(
ctx.getFeatureFlags().billing &&
cfg.isUsageBasedBilling &&
!storageService.getUsersMauAcknowledged()
);
const [operation, setOperation] = useState({
type: 'none',
} as Operation);
Expand Down Expand Up @@ -119,6 +128,11 @@ export default function useUsers({
return items.map(r => r.name);
}

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

useEffect(() => {
attemptActions.do(() => ctx.userService.fetchUsers().then(setUsers));
}, []);
Expand All @@ -143,6 +157,8 @@ export default function useUsers({
inviteCollaboratorsOpen,
onEmailPasswordResetClose,
EmailPasswordReset,
showMauInfo,
onDismissUsersMauNotice,
};
}

Expand Down
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