Skip to content

Commit

Permalink
[v15] Add notice to web UI that users arent equal to MAU
Browse files Browse the repository at this point in the history
Manual backport for #46686 because the components and styles never got
backported to v16. This feature was rebuilt for pre-v17 branches using
the old Alert component and styles.
  • Loading branch information
avatus committed Oct 2, 2024
1 parent 0549461 commit 7da0155
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 2 deletions.
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 @@ -38,6 +38,14 @@ export const Loaded = () => {
return <Users {...sample} />;
};

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

export const Failed = () => {
const attempt = {
isProcessing: false,
Expand Down Expand Up @@ -126,4 +134,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.SHOW_ASSIST_POPUP,
KeysEnum.USER_PREFERENCES,
KeysEnum.RECOMMEND_FEATURE,
KeysEnum.USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED,
];

export const storageService = {
Expand Down Expand Up @@ -218,6 +219,21 @@ export const storageService = {
);
},

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: 3 additions & 0 deletions web/packages/teleport/src/services/storageService/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const KeysEnum = {
ACCESS_GRAPH_SQL_ENABLED: 'grv_teleport_access_graph_sql_enabled',
EXTERNAL_AUDIT_STORAGE_CTA_DISABLED:
'grv_teleport_external_audit_storage_disabled',
USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED:
'grv_users_not_equal_to_mau_acknowledged',
LOCAL_NOTIFICATION_STATES: 'grv_teleport_notification_states',
};

// SurveyRequest is the request for sending data to the back end
Expand Down

0 comments on commit 7da0155

Please sign in to comment.