diff --git a/public/app/percona/shared/components/PerconaBootstrapper/PerconaBootstrapper.tsx b/public/app/percona/shared/components/PerconaBootstrapper/PerconaBootstrapper.tsx
index cd4b2fd301b95..c107e57b1a0e8 100644
--- a/public/app/percona/shared/components/PerconaBootstrapper/PerconaBootstrapper.tsx
+++ b/public/app/percona/shared/components/PerconaBootstrapper/PerconaBootstrapper.tsx
@@ -14,7 +14,9 @@ import {
fetchUserStatusAction,
setAuthorized,
} from 'app/percona/shared/core/reducers/user/user';
+import { getUpdatesInfo } from 'app/percona/shared/core/selectors';
import { useAppDispatch } from 'app/store/store';
+import { useSelector } from 'app/types';
import { Telemetry } from '../../../ui-events/components/Telemetry';
import usePerconaTour from '../../core/hooks/tour';
@@ -27,11 +29,13 @@ import { getStyles } from './PerconaBootstrapper.styles';
import { PerconaBootstrapperProps } from './PerconaBootstrapper.types';
import PerconaNavigation from './PerconaNavigation/PerconaNavigation';
import PerconaTourBootstrapper from './PerconaTour';
+import PerconaUpdateVersion from './PerconaUpdateVersion/PerconaUpdateVersion';
// This component is only responsible for populating the store with Percona's settings initially
export const PerconaBootstrapper = ({ onReady }: PerconaBootstrapperProps) => {
const dispatch = useAppDispatch();
const { setSteps, startTour: startPerconaTour, endTour } = usePerconaTour();
+ const { updateAvailable, isLoading: isLoadingUpdates, showUpdateModal } = useSelector(getUpdatesInfo);
const [modalIsOpen, setModalIsOpen] = useState(true);
const [showTour, setShowTour] = useState(false);
const styles = useStyles2(getStyles);
@@ -104,49 +108,54 @@ export const PerconaBootstrapper = ({ onReady }: PerconaBootstrapperProps) => {
{isSignedIn && }
- {isSignedIn && showTour && (
-
-
-
-
-
- {Messages.pmm}
- {Messages.pmmIs}
-
-
- {Messages.pmmEnables}
-
- - {Messages.spotCriticalPerformance}
- - {Messages.ensureDbPerformance}
- - {Messages.backup}
-
-
-
- {Messages.moreInfo}
-
- {Messages.pmmOnlineHelp}
-
- .
-
-
-
-
-
-
-
-
-
+ {updateAvailable && showUpdateModal && !isLoadingUpdates ? (
+
+ ) : (
+ isSignedIn &&
+ showTour && (
+
+
+
+
+
+ {Messages.pmm}
+ {Messages.pmmIs}
+
+
+ {Messages.pmmEnables}
+
+ - {Messages.spotCriticalPerformance}
+ - {Messages.ensureDbPerformance}
+ - {Messages.backup}
+
+
+
+ {Messages.moreInfo}
+
+ {Messages.pmmOnlineHelp}
+
+ .
+
+
+
+
+
+
+
+
+
+ )
)}
>
);
diff --git a/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.constants.ts b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.constants.ts
new file mode 100644
index 0000000000000..2f35d18455960
--- /dev/null
+++ b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.constants.ts
@@ -0,0 +1,12 @@
+export const Messages = {
+ titleOneUpdate: 'New update available',
+ titleMultipleUpdates: 'New updates available',
+ howToUpdate: 'How to update',
+ howToUpdateDescription:
+ "We are inaugurating a new process for updating PMM. It's a new interface with an improved user experience and is ready for the future of PMM. Click on Go to Updates Page to find out more.",
+ newVersions: 'New Versions',
+ readMore: 'Read more',
+ fullReleaseNotes: 'Full release notes here',
+ goToUpdatesPage: 'Go to updates page',
+ snooze: 'Dismiss',
+};
diff --git a/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.styles.ts b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.styles.ts
new file mode 100644
index 0000000000000..45dd1980d2bf0
--- /dev/null
+++ b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.styles.ts
@@ -0,0 +1,43 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+
+export const getStyles = ({ v1: { spacing, colors, typography } }: GrafanaTheme2) => ({
+ updateVersionModal: css`
+ display: flex;
+ flex-direction: column;
+ width: 480px;
+ `,
+ version: css`
+ margin-bottom: ${spacing.md};
+ `,
+ releaseNotesText: css`
+ a {
+ color: ${colors.textBlue};
+ }
+ `,
+ newVersionsTitle: css`
+ font-weight: ${typography.weight.semibold};
+ font-size: ${typography.heading.h5};
+ margin-bottom: 8px;
+ `,
+ howToUpdate: css`
+ font-weight: ${typography.weight.semibold};
+ font-size: ${typography.heading.h5};
+ margin-top: 27px;
+ `,
+ updateButtons: css`
+ margin-top: 35px;
+ display: flex;
+ justify-content: flex-end;
+ `,
+ snoozeButton: css`
+ margin-right: 20px;
+ `,
+ listOfReleaseNotes: css`
+ margin-left: 20px;
+ li a, li {
+ color: ${colors.textBlue};
+ },
+ `,
+});
diff --git a/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.test.tsx b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.test.tsx
new file mode 100644
index 0000000000000..b4b5e435cdb1b
--- /dev/null
+++ b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.test.tsx
@@ -0,0 +1,164 @@
+import { EnhancedStore } from '@reduxjs/toolkit';
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { waitFor } from 'test/test-utils';
+
+import { Messages } from 'app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.constants';
+import * as GrafanaUpdates from 'app/percona/shared/core/reducers/updates/updates';
+import * as User from 'app/percona/shared/core/reducers/user/user';
+import { UpdatesService } from 'app/percona/shared/services/updates';
+import { configureStore } from 'app/store/configureStore';
+import { StoreState } from 'app/types';
+
+import PerconaUpdateVersion from './PerconaUpdateVersion';
+
+const checkUpdatesChangeLogsSpy = jest.spyOn(GrafanaUpdates, 'checkUpdatesChangeLogs');
+const setSnoozedVersionSpy = jest.spyOn(User, 'setSnoozedVersion');
+
+describe('PerconaUpdateVersion', () => {
+ const setup = (store: EnhancedStore) =>
+ render(
+
+
+
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render modal with one update', async () => {
+ const changeLogsAPIResponse = {
+ last_check: '',
+ updates: [
+ {
+ version: 'PMM 3.0.1',
+ tag: 'string',
+ timestamp: '2024-09-23T09:12:31.488Z',
+ release_notes_url: 'http://localhost:3000',
+ release_notes_text: 'text1',
+ },
+ ],
+ };
+ const state = {
+ updates: {
+ isLoading: false,
+ updateAvailable: true,
+ latest: { version: '3.0.1' },
+ lastChecked: '',
+ showUpdateModal: true,
+ },
+ };
+ jest.spyOn(UpdatesService, 'getUpdatesChangelogs').mockReturnValue(Promise.resolve({ ...changeLogsAPIResponse }));
+
+ const defaultState = configureStore().getState();
+ const store = configureStore({
+ ...defaultState,
+ percona: {
+ ...defaultState.percona,
+ ...state,
+ },
+ } as StoreState);
+ setup(store);
+ await waitFor(() => {
+ expect(checkUpdatesChangeLogsSpy).toHaveBeenCalled();
+ });
+
+ expect(screen.queryByTestId('one-update-modal')).toBeInTheDocument();
+ expect(screen.queryByTestId('multiple-updates-modal')).not.toBeInTheDocument();
+ });
+
+ it('should render modal with multiple updates', async () => {
+ const changeLogsAPIResponse = {
+ last_check: '',
+ updates: [
+ {
+ version: 'PMM 3.0.1',
+ tag: 'string',
+ timestamp: '2024-09-27T09:12:31.488Z',
+ release_notes_url: 'http://localhost:3000',
+ release_notes_text: 'text1',
+ },
+ {
+ version: 'PMM 3.0.2',
+ tag: 'string',
+ timestamp: '2024-09-23T09:12:31.488Z',
+ release_notes_url: 'http://localhost:3000',
+ release_notes_text: 'text2',
+ },
+ ],
+ };
+ const state = {
+ updates: {
+ isLoading: false,
+ updateAvailable: true,
+ latest: { version: '3.0.1' },
+ lastChecked: '',
+ showUpdateModal: true,
+ },
+ };
+ jest.spyOn(UpdatesService, 'getUpdatesChangelogs').mockReturnValue(Promise.resolve({ ...changeLogsAPIResponse }));
+
+ const defaultState = configureStore().getState();
+ const store = configureStore({
+ ...defaultState,
+ percona: {
+ ...defaultState.percona,
+ ...state,
+ },
+ } as StoreState);
+
+ setup(store);
+ await waitFor(() => {
+ expect(checkUpdatesChangeLogsSpy).toHaveBeenCalled();
+ });
+
+ expect(screen.queryByTestId('one-update-modal')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('multiple-updates-modal')).toBeInTheDocument();
+ });
+
+ it('should dispatch setSnoozedVersion when pressing button', async () => {
+ const changeLogsAPIResponse = {
+ last_check: '',
+ updates: [
+ {
+ version: 'PMM 3.0.1',
+ tag: 'string',
+ timestamp: '2024-09-23T09:12:31.488Z',
+ release_notes_url: 'http://localhost:3000',
+ release_notes_text: 'text1',
+ },
+ ],
+ };
+ const state = {
+ updates: {
+ isLoading: false,
+ updateAvailable: true,
+ latest: { version: '3.0.1' },
+ lastChecked: '',
+ showUpdateModal: true,
+ },
+ };
+ jest.spyOn(UpdatesService, 'getUpdatesChangelogs').mockReturnValue(Promise.resolve({ ...changeLogsAPIResponse }));
+
+ const defaultState = configureStore().getState();
+ const store = configureStore({
+ ...defaultState,
+ percona: {
+ ...defaultState.percona,
+ ...state,
+ },
+ } as StoreState);
+ setup(store);
+ await waitFor(() => {
+ expect(checkUpdatesChangeLogsSpy).toHaveBeenCalled();
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: Messages.snooze }));
+
+ await waitFor(() => {
+ expect(setSnoozedVersionSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.tsx b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.tsx
new file mode 100644
index 0000000000000..e5ad28d961162
--- /dev/null
+++ b/public/app/percona/shared/components/PerconaBootstrapper/PerconaUpdateVersion/PerconaUpdateVersion.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect } from 'react';
+
+import { dateTimeFormat } from '@grafana/data';
+import { Modal, useStyles2, Button } from '@grafana/ui';
+import { PMM_UPDATES_LINK } from 'app/percona/shared/components/PerconaBootstrapper/PerconaNavigation';
+import {
+ checkUpdatesChangeLogs,
+ setShowUpdateModal,
+ UpdatesChangeLogs,
+} from 'app/percona/shared/core/reducers/updates';
+import { setSnoozedVersion } from 'app/percona/shared/core/reducers/user/user';
+import { getPerconaUser, getUpdatesInfo } from 'app/percona/shared/core/selectors';
+import { useAppDispatch } from 'app/store/store';
+import { useSelector } from 'app/types';
+
+import { Messages } from './PerconaUpdateVersion.constants';
+import { getStyles } from './PerconaUpdateVersion.styles';
+
+const PerconaUpdateVersion = () => {
+ const { updateAvailable, installed, latest, changeLogs, showUpdateModal } = useSelector(getUpdatesInfo);
+ const { snoozedPmmVersion } = useSelector(getPerconaUser);
+ const dispatch = useAppDispatch();
+ const styles = useStyles2(getStyles);
+
+ useEffect(() => {
+ const prepareModal = async () => {
+ if (installed?.version === latest?.version || snoozedPmmVersion === latest?.version) {
+ dispatch(setShowUpdateModal(false));
+ } else {
+ await dispatch(checkUpdatesChangeLogs());
+ }
+ };
+
+ if (updateAvailable) {
+ prepareModal();
+ }
+ }, [dispatch, updateAvailable, installed, latest, snoozedPmmVersion]);
+
+ const snoozeUpdate = async () => {
+ if (latest && latest.version) {
+ await dispatch(setSnoozedVersion(latest.version));
+ }
+ dispatch(setShowUpdateModal(false));
+ };
+
+ const onDismiss = () => {
+ dispatch(setShowUpdateModal(false));
+ };
+
+ const onUpdateClick = () => {
+ dispatch(setShowUpdateModal(false));
+ window.location.assign(PMM_UPDATES_LINK.url!);
+ };
+
+ return (
+ <>
+
+
+
{latest?.version || ''}
+
+
+ {Messages.fullReleaseNotes}
+
+
+
{Messages.howToUpdate}
+
{Messages.howToUpdateDescription}
+
+
+
+
+
+
+ 1}
+ className={styles.updateVersionModal}
+ >
+
+
{Messages.newVersions}
+
+
{Messages.howToUpdate}
+
{Messages.howToUpdateDescription}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default PerconaUpdateVersion;
diff --git a/public/app/percona/shared/core/reducers/updates/updates.ts b/public/app/percona/shared/core/reducers/updates/updates.ts
index 966377b8714f9..a157227af1aef 100644
--- a/public/app/percona/shared/core/reducers/updates/updates.ts
+++ b/public/app/percona/shared/core/reducers/updates/updates.ts
@@ -1,12 +1,13 @@
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import { createAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { UpdatesService } from 'app/percona/shared/services/updates';
import { CheckUpdatesPayload, UpdatesState } from './updates.types';
-import { responseToPayload } from './updates.utils';
+import { mapUpdatesChangeLogs, responseToPayload } from './updates.utils';
const initialState: UpdatesState = {
isLoading: false,
+ showUpdateModal: true,
};
export const updatesSlice = createSlice({
@@ -18,17 +19,29 @@ export const updatesSlice = createSlice({
...initialState,
isLoading: true,
}));
-
builder.addCase(checkUpdatesAction.fulfilled, (state, { payload }) => ({
...state,
...payload,
isLoading: false,
}));
-
- builder.addCase(checkUpdatesAction.rejected, () => ({
- ...initialState,
+ builder.addCase(checkUpdatesAction.rejected, (state) => ({
+ ...state,
isLoading: false,
}));
+ builder.addCase(checkUpdatesChangeLogs.pending, (state) => ({
+ ...state,
+ }));
+ builder.addCase(checkUpdatesChangeLogs.fulfilled, (state, { payload }) => ({
+ ...state,
+ changeLogs: payload,
+ }));
+ builder.addCase(checkUpdatesChangeLogs.rejected, (state) => ({
+ ...state,
+ }));
+ builder.addCase(setShowUpdateModal, (state, { payload }) => ({
+ ...state,
+ showUpdateModal: payload,
+ }));
},
});
@@ -42,4 +55,10 @@ export const checkUpdatesAction = createAsyncThunk('percona/checkUpdates', async
}
});
+export const checkUpdatesChangeLogs = createAsyncThunk('percona/checkUpdatesChangelogs', async () => {
+ return mapUpdatesChangeLogs(await UpdatesService.getUpdatesChangelogs());
+});
+
+export const setShowUpdateModal = createAction('percona/setShowUpdateModal');
+
export default updatesSlice.reducer;
diff --git a/public/app/percona/shared/core/reducers/updates/updates.types.ts b/public/app/percona/shared/core/reducers/updates/updates.types.ts
index 5c6fb31a09cc0..15090bac21396 100644
--- a/public/app/percona/shared/core/reducers/updates/updates.types.ts
+++ b/public/app/percona/shared/core/reducers/updates/updates.types.ts
@@ -17,6 +17,8 @@ export interface UpdatesState {
latest?: LatestInformation;
latestNewsUrl?: string;
lastChecked?: string;
+ changeLogs?: CheckUpdatesChangeLogs;
+ showUpdateModal: boolean;
}
export interface CheckUpdatesPayload {
@@ -26,3 +28,29 @@ export interface CheckUpdatesPayload {
lastChecked?: string;
updateAvailable: boolean;
}
+
+export interface UpdatesChangeLogsResponse {
+ version: string;
+ tag: string;
+ timestamp: string;
+ release_notes_url: string;
+ release_notes_text: string;
+}
+
+export interface UpdatesChangeLogs {
+ version: string;
+ tag: string;
+ timestamp: string;
+ releaseNotesUrl: string;
+ releaseNotesText: string;
+}
+
+export interface CheckUpdatesChangeLogs {
+ updates: UpdatesChangeLogs[];
+ lastCheck: string;
+}
+
+export interface CheckUpdatesChangeLogsResponse {
+ updates: UpdatesChangeLogsResponse[];
+ last_check: string;
+}
diff --git a/public/app/percona/shared/core/reducers/updates/updates.utils.ts b/public/app/percona/shared/core/reducers/updates/updates.utils.ts
index 10dd6c6756a7d..cf99f4bcfdc6e 100644
--- a/public/app/percona/shared/core/reducers/updates/updates.utils.ts
+++ b/public/app/percona/shared/core/reducers/updates/updates.utils.ts
@@ -1,6 +1,6 @@
import { CheckUpdatesResponse } from 'app/percona/shared/services/updates/Updates.types';
-import { CheckUpdatesPayload } from './updates.types';
+import { CheckUpdatesPayload, CheckUpdatesChangeLogs, CheckUpdatesChangeLogsResponse } from './updates.types';
export const responseToPayload = (response: CheckUpdatesResponse): CheckUpdatesPayload => ({
installed: response.installed
@@ -21,3 +21,17 @@ export const responseToPayload = (response: CheckUpdatesResponse): CheckUpdatesP
latestNewsUrl: response.latest_news_url,
updateAvailable: !!response.update_available,
});
+
+export const mapUpdatesChangeLogs = (response: CheckUpdatesChangeLogsResponse): CheckUpdatesChangeLogs => {
+ const responseMapping = response.updates.map((item) => ({
+ version: item.version,
+ tag: item.tag,
+ timestamp: item.timestamp,
+ releaseNotesUrl: item.release_notes_url,
+ releaseNotesText: item.release_notes_text,
+ }));
+ return {
+ lastCheck: response.last_check,
+ updates: responseMapping,
+ };
+};
diff --git a/public/app/percona/shared/core/reducers/user/user.ts b/public/app/percona/shared/core/reducers/user/user.ts
index c9bb28e015d7a..d8391e0f948fe 100644
--- a/public/app/percona/shared/core/reducers/user/user.ts
+++ b/public/app/percona/shared/core/reducers/user/user.ts
@@ -13,6 +13,7 @@ export const initialUserState: PerconaUserState = {
alertingTourCompleted: true,
isAuthorized: false,
isPlatformUser: false,
+ snoozedPmmVersion: '',
};
const perconaUserSlice = createSlice({
@@ -65,6 +66,16 @@ export const setProductTourCompleted = createAsyncThunk(
}
);
+export const setSnoozedVersion = createAsyncThunk(
+ 'percona/setProductTourCompleted',
+ async (version: string, thunkAPI): Promise => {
+ const res = await UserService.setSnoozedVersion(version);
+ const details = toUserDetailsModel(res);
+ thunkAPI.dispatch(setUserDetails(details));
+ return details;
+ }
+);
+
export const setAlertingTourCompleted = createAsyncThunk(
'percona/setAlertingTourCompleted',
async (alertingTourCompleted: boolean, thunkAPI): Promise => {
diff --git a/public/app/percona/shared/core/reducers/user/user.types.ts b/public/app/percona/shared/core/reducers/user/user.types.ts
index 8b1734333170d..c8a950d900c5a 100644
--- a/public/app/percona/shared/core/reducers/user/user.types.ts
+++ b/public/app/percona/shared/core/reducers/user/user.types.ts
@@ -2,6 +2,7 @@ export interface UserDetails {
userId: number;
productTourCompleted: boolean;
alertingTourCompleted: boolean;
+ snoozedPmmVersion?: string;
}
export interface PerconaUserState extends UserDetails {
diff --git a/public/app/percona/shared/core/reducers/user/user.utils.ts b/public/app/percona/shared/core/reducers/user/user.utils.ts
index d4c10723bf56f..5e972077d1bd8 100644
--- a/public/app/percona/shared/core/reducers/user/user.utils.ts
+++ b/public/app/percona/shared/core/reducers/user/user.utils.ts
@@ -6,4 +6,5 @@ export const toUserDetailsModel = (res: UserDetailsResponse): UserDetails => ({
userId: res.user_id,
productTourCompleted: !!res.product_tour_completed,
alertingTourCompleted: !!res.alerting_tour_completed,
+ snoozedPmmVersion: res.snoozed_pmm_version,
});
diff --git a/public/app/percona/shared/services/updates/Updates.service.ts b/public/app/percona/shared/services/updates/Updates.service.ts
index b4ef6cb681ef5..9de9719150369 100644
--- a/public/app/percona/shared/services/updates/Updates.service.ts
+++ b/public/app/percona/shared/services/updates/Updates.service.ts
@@ -1,8 +1,12 @@
+import { CheckUpdatesChangeLogsResponse } from 'app/percona/shared/core/reducers/updates';
+
import { api } from '../../helpers/api';
import { CheckUpdatesParams, CheckUpdatesResponse } from './Updates.types';
export const UpdatesService = {
- getCurrentVersion: (params: CheckUpdatesParams = { force: false }) =>
- api.get('/v1/server/updates', true, { params }),
+ getCurrentVersion: (body: CheckUpdatesParams = { force: false }) =>
+ api.get('/v1/server/updates', true, { params: body }),
+
+ getUpdatesChangelogs: () => api.get('/v1/server/updates/changelogs', false),
};
diff --git a/public/app/percona/shared/services/user/User.service.ts b/public/app/percona/shared/services/user/User.service.ts
index 2759cc2c6495d..eeefbe6be117c 100644
--- a/public/app/percona/shared/services/user/User.service.ts
+++ b/public/app/percona/shared/services/user/User.service.ts
@@ -22,6 +22,10 @@ export const UserService = {
const payload: UserDetailsPutPayload = { alerting_tour_completed: completed };
return await api.put('/v1/users/me', payload);
},
+ async setSnoozedVersion(version: string): Promise {
+ const payload: UserDetailsPutPayload = { snoozed_pmm_version: version };
+ return await api.put('/v1/users/me', payload);
+ },
async getUsersList(): Promise {
return await api.get('/v1/users');
},
diff --git a/public/app/percona/shared/services/user/User.types.ts b/public/app/percona/shared/services/user/User.types.ts
index 86c22adb08803..a31cadb6fc239 100644
--- a/public/app/percona/shared/services/user/User.types.ts
+++ b/public/app/percona/shared/services/user/User.types.ts
@@ -6,11 +6,13 @@ export interface UserDetailsResponse {
user_id: number;
product_tour_completed?: boolean;
alerting_tour_completed?: boolean;
+ snoozed_pmm_version?: string;
}
export interface UserDetailsPutPayload {
product_tour_completed?: boolean;
alerting_tour_completed?: boolean;
+ snoozed_pmm_version?: string;
}
export interface UserListItemResponse {