[8.8] [Synthetics] Handle errors in global params UI (#156722) (#156999)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Synthetics] Handle errors in global params UI
(#156722)](https://github.com/elastic/kibana/pull/156722)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2023-05-08T14:01:26Z","message":"[Synthetics]
Handle errors in global params UI
(#156722)","sha":"0ee2a169539443ebac1371e3e9df045472451f72","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:uptime","release_note:skip","v8.8.0","v8.9.0"],"number":156722,"url":"https://github.com/elastic/kibana/pull/156722","mergeCommit":{"message":"[Synthetics]
Handle errors in global params UI
(#156722)","sha":"0ee2a169539443ebac1371e3e9df045472451f72"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/156722","number":156722,"mergeCommit":{"message":"[Synthetics]
Handle errors in global params UI
(#156722)","sha":"0ee2a169539443ebac1371e3e9df045472451f72"}}]}]
BACKPORT-->

Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Kibana Machine 2023-05-08 12:11:10 -04:00 committed by GitHub
parent 1612cedcea
commit d07529fc5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 304 additions and 82 deletions

View file

@ -20,26 +20,27 @@ import {
} from '@elastic/eui';
import { FormProvider } from 'react-hook-form';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useFetcher } from '@kbn/observability-plugin/public';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { apiService } from '../../../../../utils/api_service';
import { useDispatch, useSelector } from 'react-redux';
import {
addNewGlobalParamAction,
editGlobalParamAction,
getGlobalParamAction,
selectGlobalParamState,
} from '../../../state/global_params';
import { ClientPluginsStart } from '../../../../../plugin';
import { ListParamItem } from './params_list';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
import { AddParamForm } from './add_param_form';
import { SYNTHETICS_API_URLS } from '../../../../../../common/constants';
import { syncGlobalParamsAction } from '../../../state/settings';
export const AddParamFlyout = ({
items,
isEditingItem,
setIsEditingItem,
setRefreshList,
}: {
items: ListParamItem[];
setRefreshList: React.Dispatch<React.SetStateAction<number>>;
isEditingItem: ListParamItem | null;
setIsEditingItem: React.Dispatch<React.SetStateAction<ListParamItem | null>>;
}) => {
@ -67,45 +68,42 @@ export const AddParamFlyout = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsEditingItem]);
const [paramData, setParamData] = useState<SyntheticsParamSO | null>(null);
const { application } = useKibana<ClientPluginsStart>().services;
const { loading, data } = useFetcher(async () => {
if (!paramData) {
return;
}
const { namespaces, ...paramRequest } = paramData;
const shareAcrossSpaces = namespaces?.includes(ALL_SPACES_ID);
if (isEditingItem) {
return apiService.put(SYNTHETICS_API_URLS.PARAMS, {
id,
...paramRequest,
share_across_spaces: shareAcrossSpaces,
});
}
return apiService.post(SYNTHETICS_API_URLS.PARAMS, {
...paramRequest,
share_across_spaces: shareAcrossSpaces,
});
}, [paramData]);
const canSave = (application?.capabilities.uptime.save ?? false) as boolean;
const onSubmit = (formData: SyntheticsParamSO) => {
setParamData(formData);
};
const dispatch = useDispatch();
const { isSaving, savedData } = useSelector(selectGlobalParamState);
const onSubmit = (formData: SyntheticsParamSO) => {
const { namespaces, ...paramRequest } = formData;
const shareAcrossSpaces = namespaces?.includes(ALL_SPACES_ID);
if (isEditingItem && id) {
dispatch(
editGlobalParamAction.get({
id,
paramRequest: { ...paramRequest, share_across_spaces: shareAcrossSpaces },
})
);
} else {
dispatch(
addNewGlobalParamAction.get({
...paramRequest,
share_across_spaces: shareAcrossSpaces,
})
);
}
};
useEffect(() => {
if (data && !loading) {
if (savedData && !isSaving) {
closeFlyout();
setRefreshList(Date.now());
setParamData(null);
dispatch(getGlobalParamAction.get());
dispatch(syncGlobalParamsAction.get());
}
}, [data, loading, closeFlyout, setRefreshList, dispatch]);
}, [savedData, isSaving, closeFlyout, dispatch]);
useEffect(() => {
if (isEditingItem) {
@ -150,7 +148,7 @@ export const AddParamFlyout = ({
data-test-subj="syntheticsAddParamFlyoutButton"
onClick={handleSubmit(onSubmit)}
fill
isLoading={loading}
isLoading={isSaving}
>
{SAVE_TABLE}
</EuiButton>

View file

@ -12,6 +12,7 @@ import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { getGlobalParamAction } from '../../../state/global_params';
import { syncGlobalParamsAction } from '../../../state/settings';
import { kibanaService } from '../../../../../utils/kibana_service';
import { syntheticsParamType } from '../../../../../../common/types/saved_objects';
@ -19,11 +20,9 @@ import { NO_LABEL, YES_LABEL } from '../../monitors_page/management/monitor_list
import { ListParamItem } from './params_list';
export const DeleteParam = ({
setRefreshList,
items,
setIsDeleteModalVisible,
}: {
setRefreshList: React.Dispatch<React.SetStateAction<number>>;
items: ListParamItem[];
setIsDeleteModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
@ -89,9 +88,10 @@ export const DeleteParam = ({
if (status === FETCH_STATUS.SUCCESS || status === FETCH_STATUS.FAILURE) {
setIsDeleting(false);
setIsDeleteModalVisible(false);
setRefreshList(Date.now());
dispatch(getGlobalParamAction.get());
dispatch(syncGlobalParamsAction.get());
}
}, [setIsDeleting, isDeleting, status, setIsDeleteModalVisible, name, setRefreshList, dispatch]);
}, [setIsDeleting, isDeleting, status, setIsDeleteModalVisible, name, dispatch]);
return (
<EuiConfirmModal

View file

@ -34,12 +34,10 @@ export interface ListParamItem extends SyntheticsParamSO {
}
export const ParamsList = () => {
const [refreshList, setRefreshList] = useState(Date.now());
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const { items, loading } = useParamsList(refreshList);
const { items, isLoading } = useParamsList();
const [isEditingItem, setIsEditingItem] = useState<ListParamItem | null>(null);
@ -182,7 +180,6 @@ export const ParamsList = () => {
<AddParamFlyout
isEditingItem={isEditingItem}
setIsEditingItem={setIsEditingItem}
setRefreshList={setRefreshList}
items={items}
/>,
];
@ -235,7 +232,7 @@ export const ParamsList = () => {
<EuiSpacer size="m" />
<EuiInMemoryTable<ListParamItem>
itemId="id"
loading={loading}
loading={isLoading}
tableCaption={PARAMS_TABLE}
items={filteredItems}
columns={columns}
@ -285,14 +282,10 @@ export const ParamsList = () => {
},
],
}}
message={loading ? LOADING_TEXT : undefined}
message={isLoading ? LOADING_TEXT : undefined}
/>
{isDeleteModalVisible && deleteParam && (
<DeleteParam
items={deleteParam}
setIsDeleteModalVisible={setIsDeleteModalVisible}
setRefreshList={setRefreshList}
/>
<DeleteParam items={deleteParam} setIsDeleteModalVisible={setIsDeleteModalVisible} />
)}
</div>
);

View file

@ -5,29 +5,28 @@
* 2.0.
*/
import { useFetcher } from '@kbn/observability-plugin/public';
import { SavedObject } from '@kbn/core-saved-objects-common';
import { useMemo } from 'react';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { apiService } from '../../../../../utils/api_service';
import { SYNTHETICS_API_URLS } from '../../../../../../common/constants';
import { useMemo, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getGlobalParamAction, selectGlobalParamState } from '../../../state/global_params';
export const useParamsList = (lastRefresh: number) => {
const { data, loading } = useFetcher<
Promise<{ data: Array<SavedObject<SyntheticsParamSO>> }>
>(() => {
return apiService.get(SYNTHETICS_API_URLS.PARAMS);
}, [lastRefresh]);
export const useParamsList = () => {
const { isLoading, listOfParams } = useSelector(selectGlobalParamState);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalParamAction.get());
}, [dispatch]);
return useMemo(() => {
return {
items:
data?.data.map((item) => ({
listOfParams?.map((item) => ({
id: item.id,
...item.attributes,
namespaces: item.namespaces,
})) ?? [],
loading,
isLoading,
};
}, [data, loading]);
}, [listOfParams, isLoading]);
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from '@kbn/core-saved-objects-common';
import { SyntheticsParamRequest, SyntheticsParamSO } from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';
export const getGlobalParamAction = createAsyncAction<void, Array<SavedObject<SyntheticsParamSO>>>(
'GET GLOBAL PARAMS'
);
export const addNewGlobalParamAction = createAsyncAction<SyntheticsParamRequest, SyntheticsParamSO>(
'ADD NEW GLOBAL PARAM'
);
export const editGlobalParamAction = createAsyncAction<
{
id: string;
paramRequest: SyntheticsParamRequest;
},
SyntheticsParamSO
>('EDIT GLOBAL PARAM');

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from '@kbn/core-saved-objects-common';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { SyntheticsParamRequest, SyntheticsParamSO } from '../../../../../common/runtime_types';
import { apiService } from '../../../../utils/api_service/api_service';
export const getGlobalParams = async (): Promise<Array<SavedObject<SyntheticsParamSO>>> => {
const result = (await apiService.get(SYNTHETICS_API_URLS.PARAMS)) as {
data: Array<SavedObject<SyntheticsParamSO>>;
};
return result.data;
};
export const addGlobalParam = async (
paramRequest: SyntheticsParamRequest
): Promise<SyntheticsParamSO> => {
return apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest);
};
export const editGlobalParam = async ({
paramRequest,
id,
}: {
id: string;
paramRequest: SyntheticsParamRequest;
}): Promise<SyntheticsParamSO> => {
return apiService.put(SYNTHETICS_API_URLS.PARAMS, {
id,
...paramRequest,
});
};

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { takeLeading } from 'redux-saga/effects';
import { i18n } from '@kbn/i18n';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { addGlobalParam, editGlobalParam, getGlobalParams } from './api';
import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions';
export function* getGlobalParamEffect() {
yield takeLeading(
getGlobalParamAction.get,
fetchEffectFactory(
getGlobalParams,
getGlobalParamAction.success,
getGlobalParamAction.fail,
undefined,
getFailMessage
)
);
}
const getFailMessage = i18n.translate('xpack.synthetics.settings.getParams.failed', {
defaultMessage: 'Failed to get global parameters.',
});
export function* addGlobalParamEffect() {
yield takeLeading(
addNewGlobalParamAction.get,
fetchEffectFactory(
addGlobalParam,
addNewGlobalParamAction.success,
addNewGlobalParamAction.fail,
successMessage,
failureMessage
)
);
}
const successMessage = i18n.translate('xpack.synthetics.settings.addParams.success', {
defaultMessage: 'Successfully added global parameter.',
});
const failureMessage = i18n.translate('xpack.synthetics.settings.addParams.fail', {
defaultMessage: 'Failed to add global parameter.',
});
export function* editGlobalParamEffect() {
yield takeLeading(
editGlobalParamAction.get,
fetchEffectFactory(
editGlobalParam,
editGlobalParamAction.success,
editGlobalParamAction.fail,
editSuccessMessage,
editFailureMessage
)
);
}
const editSuccessMessage = i18n.translate('xpack.synthetics.settings.editParams.success', {
defaultMessage: 'Successfully edited global parameter.',
});
const editFailureMessage = i18n.translate('xpack.synthetics.settings.editParams.fail', {
defaultMessage: 'Failed to edit global parameter.',
});

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createReducer } from '@reduxjs/toolkit';
import { SavedObject } from '@kbn/core-saved-objects-common';
import { SyntheticsParamSO } from '../../../../../common/runtime_types';
import { IHttpSerializedFetchError } from '..';
import { addNewGlobalParamAction, editGlobalParamAction, getGlobalParamAction } from './actions';
export interface GlobalParamsState {
isLoading?: boolean;
listOfParams?: Array<SavedObject<SyntheticsParamSO>>;
addError: IHttpSerializedFetchError | null;
editError: IHttpSerializedFetchError | null;
isSaving?: boolean;
savedData?: SyntheticsParamSO;
}
const initialState: GlobalParamsState = {
isLoading: false,
addError: null,
isSaving: false,
editError: null,
listOfParams: [],
};
export const globalParamsReducer = createReducer(initialState, (builder) => {
builder
.addCase(getGlobalParamAction.get, (state) => {
state.isLoading = true;
})
.addCase(getGlobalParamAction.success, (state, action) => {
state.isLoading = false;
state.listOfParams = action.payload;
})
.addCase(getGlobalParamAction.fail, (state, action) => {
state.isLoading = false;
})
.addCase(addNewGlobalParamAction.get, (state) => {
state.isSaving = true;
state.savedData = undefined;
})
.addCase(addNewGlobalParamAction.success, (state, action) => {
state.isSaving = false;
state.savedData = action.payload;
})
.addCase(addNewGlobalParamAction.fail, (state, action) => {
state.isSaving = false;
state.addError = action.payload;
})
.addCase(editGlobalParamAction.get, (state) => {
state.isSaving = true;
state.savedData = undefined;
})
.addCase(editGlobalParamAction.success, (state, action) => {
state.isSaving = false;
state.savedData = action.payload;
})
.addCase(editGlobalParamAction.fail, (state, action) => {
state.isSaving = false;
state.editError = action.payload;
});
});
export * from './actions';
export * from './effects';
export * from './selectors';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AppState } from '..';
export const selectGlobalParamState = (state: AppState) => state.globalParams;

View file

@ -6,6 +6,7 @@
*/
import { all, fork } from 'redux-saga/effects';
import { addGlobalParamEffect, editGlobalParamEffect, getGlobalParamEffect } from './global_params';
import { fetchManualTestRunsEffect } from './manual_test_runs/effects';
import { enableDefaultAlertingEffect, updateDefaultAlertingEffect } from './alert_rules/effects';
import { executeEsQueryEffect } from './elasticsearch';
@ -53,5 +54,8 @@ export const rootEffect = function* root(): Generator {
fork(executeEsQueryEffect),
fork(fetchJourneyStepsEffect),
fork(fetchManualTestRunsEffect),
fork(addGlobalParamEffect),
fork(editGlobalParamEffect),
fork(getGlobalParamEffect),
]);
};

View file

@ -7,6 +7,7 @@
import { combineReducers } from '@reduxjs/toolkit';
import { globalParamsReducer, GlobalParamsState } from './global_params';
import { overviewStatusReducer, OverviewStatusStateReducer } from './overview_status';
import { browserJourneyReducer } from './browser_journey';
import { defaultAlertingReducer, DefaultAlertingState } from './alert_rules';
@ -36,6 +37,7 @@ export interface SyntheticsAppState {
elasticsearch: QueriesState;
monitorList: MonitorListState;
overview: MonitorOverviewState;
globalParams: GlobalParamsState;
networkEvents: NetworkEventsState;
agentPolicies: AgentPoliciesState;
manualTestRuns: ManualTestRunsState;
@ -54,6 +56,7 @@ export const rootReducer = combineReducers<SyntheticsAppState>({
pingStatus: pingStatusReducer,
monitorList: monitorListReducer,
overview: monitorOverviewReducer,
globalParams: globalParamsReducer,
networkEvents: networkEventsReducer,
elasticsearch: elasticsearchReducer,
agentPolicies: agentPoliciesReducer,

View file

@ -61,21 +61,33 @@ export function fetchEffectFactory<T, R, S, F>(
onSuccess?: ((response: R) => void) | string,
onFailure?: ((error: Error) => void) | string
) {
const showErrorToast = (error: Error, action: PayloadAction<T>) => {
const serializedError = serializeHttpFetchError(error as IHttpFetchError, action.payload);
if (typeof onFailure === 'function') {
onFailure?.(error);
} else if (typeof onFailure === 'string') {
kibanaService.core.notifications.toasts.addError(
{ ...error, message: serializedError.body?.message ?? error.message },
{
title: onFailure,
}
);
}
};
return function* (action: PayloadAction<T>): Generator {
try {
const response = yield call(fetch, action.payload);
if (response instanceof Error) {
const error = response as Error;
// eslint-disable-next-line no-console
console.error(response);
console.error(error);
yield put(fail(serializeHttpFetchError(response as IHttpFetchError, action.payload)));
if (typeof onFailure === 'function') {
onFailure?.(response);
} else if (typeof onFailure === 'string') {
kibanaService.core.notifications.toasts.addError(response, {
title: onFailure,
});
}
const serializedError = serializeHttpFetchError(error as IHttpFetchError, action.payload);
yield put(fail(serializedError));
showErrorToast(error, action);
} else {
yield put(success(response as R));
const successMessage = (action.payload as unknown as ActionMessages)?.success;
@ -108,13 +120,7 @@ export function fetchEffectFactory<T, R, S, F>(
}
yield put(fail(serializeHttpFetchError(error, action.payload)));
if (typeof onFailure === 'function') {
onFailure?.(error);
} else if (typeof onFailure === 'string') {
kibanaService.core.notifications.toasts.addError(error, {
title: onFailure,
});
}
showErrorToast(error, action);
}
};
}

View file

@ -141,6 +141,10 @@ export const mockState: SyntheticsAppState = {
status: null,
error: null,
},
globalParams: {
addError: null,
editError: null,
},
};
function getBrowserJourneyMockSlice() {