diff --git a/src/legacy/ui/public/management/index.d.ts b/src/legacy/ui/public/management/index.d.ts
index ed4658f82dea38..02e98b30f59048 100644
--- a/src/legacy/ui/public/management/index.d.ts
+++ b/src/legacy/ui/public/management/index.d.ts
@@ -28,4 +28,5 @@ declare module 'ui/management' {
allowOverride: boolean
): void;
export const management: any; // TODO - properly provide types
+ export const MANAGEMENT_BREADCRUMB: any;
}
diff --git a/x-pack/plugins/watcher/public/components/page_error/index.ts b/x-pack/plugins/watcher/public/components/page_error/index.ts
new file mode 100644
index 00000000000000..cf350b1b989817
--- /dev/null
+++ b/x-pack/plugins/watcher/public/components/page_error/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getPageErrorCode, PageError } from './page_error';
diff --git a/x-pack/plugins/watcher/public/components/page_error/page_error.tsx b/x-pack/plugins/watcher/public/components/page_error/page_error.tsx
new file mode 100644
index 00000000000000..429adccfe2acd8
--- /dev/null
+++ b/x-pack/plugins/watcher/public/components/page_error/page_error.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { PageErrorNotExist } from './page_error_not_exist';
+import { PageErrorForbidden } from './page_error_forbidden';
+
+export function getPageErrorCode(errorOrErrors: any) {
+ const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors];
+ const firstError = errors.find((error: any) => {
+ if (error) {
+ return [403, 404].includes(error.status);
+ }
+
+ return false;
+ });
+
+ if (firstError) {
+ return firstError.status;
+ }
+}
+
+export function PageError({ errorCode, id }: { errorCode?: any; id?: any }) {
+ switch (errorCode) {
+ case 404:
+ return ;
+
+ case 403:
+ default:
+ return ;
+ }
+
+ return null;
+}
diff --git a/x-pack/plugins/watcher/public/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/watcher/public/components/page_error/page_error_forbidden.tsx
new file mode 100644
index 00000000000000..1561660aaee80d
--- /dev/null
+++ b/x-pack/plugins/watcher/public/components/page_error/page_error_forbidden.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { EuiEmptyPrompt } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export function PageErrorForbidden() {
+ return (
+
+
+
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/watcher/public/components/page_error/page_error_not_exist.tsx b/x-pack/plugins/watcher/public/components/page_error/page_error_not_exist.tsx
new file mode 100644
index 00000000000000..662ecaafadbb40
--- /dev/null
+++ b/x-pack/plugins/watcher/public/components/page_error/page_error_not_exist.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { EuiEmptyPrompt } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export function PageErrorNotExist({ id }: { id: any }) {
+ return (
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/watcher/public/lib/api.ts b/x-pack/plugins/watcher/public/lib/api.ts
index d1ed0e7919ff50..969884ef3811bd 100644
--- a/x-pack/plugins/watcher/public/lib/api.ts
+++ b/x-pack/plugins/watcher/public/lib/api.ts
@@ -11,56 +11,69 @@ import { __await } from 'tslib';
import chrome from 'ui/chrome';
import { ROUTES } from '../../common/constants';
import { BaseWatch, ExecutedWatchDetails } from '../../common/types/watch_types';
+import { useRequest } from './use_request';
let httpClient: ng.IHttpService;
+
export const setHttpClient = (anHttpClient: ng.IHttpService) => {
httpClient = anHttpClient;
};
+
export const getHttpClient = () => {
return httpClient;
};
+
let savedObjectsClient: any;
+
export const setSavedObjectsClient = (aSavedObjectsClient: any) => {
savedObjectsClient = aSavedObjectsClient;
};
+
export const getSavedObjectsClient = () => {
return savedObjectsClient;
};
+
const basePath = chrome.addBasePath(ROUTES.API_ROOT);
-export const fetchWatches = async () => {
- const {
- data: { watches },
- } = await getHttpClient().get(`${basePath}/watches`);
- return watches.map((watch: any) => {
- return Watch.fromUpstreamJson(watch);
+
+export const loadWatches = (interval: number) => {
+ return useRequest({
+ path: `${basePath}/watches`,
+ method: 'get',
+ interval,
+ processData: ({ watches }: { watches: any }) =>
+ watches.map((watch: any) => Watch.fromUpstreamJson(watch)),
});
};
-export const fetchWatchDetail = async (id: string) => {
- const {
- data: { watch },
- } = await getHttpClient().get(`${basePath}/watch/${id}`);
- return Watch.fromUpstreamJson(watch);
+export const loadWatchDetail = (id: string) => {
+ return useRequest({
+ path: `${basePath}/watch/${id}`,
+ method: 'get',
+ processData: ({ watch }: { watch: any }) => Watch.fromUpstreamJson(watch),
+ });
};
-export const fetchWatchHistoryDetail = async (id: string) => {
- const {
- data: { watchHistoryItem },
- } = await getHttpClient().get(`${basePath}/history/${id}`);
- const item = WatchHistoryItem.fromUpstreamJson(watchHistoryItem);
- return item;
-};
+export const loadWatchHistory = (id: string, startTime: string) => {
+ let path = `${basePath}/watch/${id}/history`;
-export const fetchWatchHistory = async (id: string, startTime: string) => {
- let url = `${basePath}/watch/${id}/history`;
if (startTime) {
- url += `?startTime=${startTime}`;
+ path += `?startTime=${startTime}`;
}
- const result: any = await getHttpClient().get(url);
- const items: any = result.data.watchHistoryItems;
- return items.map((historyItem: any) => {
- const item = WatchHistoryItem.fromUpstreamJson(historyItem);
- return item;
+
+ return useRequest({
+ path,
+ method: 'get',
+ processData: ({ watchHistoryItems: items }: { watchHistoryItems: any }) =>
+ items.map((historyItem: any) => WatchHistoryItem.fromUpstreamJson(historyItem)),
+ });
+};
+
+export const loadWatchHistoryDetail = (id: string | undefined) => {
+ return useRequest({
+ path: !id ? undefined : `${basePath}/history/${id}`,
+ method: 'get',
+ processData: ({ watchHistoryItem }: { watchHistoryItem: any }) =>
+ WatchHistoryItem.fromUpstreamJson(watchHistoryItem),
});
};
@@ -97,10 +110,12 @@ export const fetchWatch = async (watchId: string) => {
} = await getHttpClient().post(`${basePath}/watches/`, body);
return results;
};
+
export const loadWatch = async (id: string) => {
const { data: watch } = await getHttpClient().get(`${basePath}/watch/${id}`);
return Watch.fromUpstreamJson(watch.watch);
};
+
export const getMatchingIndices = async (pattern: string) => {
if (!pattern.startsWith('*')) {
pattern = `*${pattern}`;
@@ -113,16 +128,19 @@ export const getMatchingIndices = async (pattern: string) => {
} = await getHttpClient().post(`${basePath}/indices`, { pattern });
return indices;
};
+
export const fetchFields = async (indexes: string[]) => {
const {
data: { fields },
} = await getHttpClient().post(`${basePath}/fields`, { indexes });
return fields;
};
+
export const createWatch = async (watch: BaseWatch) => {
const { data } = await getHttpClient().put(`${basePath}/watch/${watch.id}`, watch.upstreamJson);
return data;
};
+
export const executeWatch = async (executeWatchDetails: ExecutedWatchDetails, watch: BaseWatch) => {
const { data } = await getHttpClient().put(`${basePath}/watch/execute`, {
executeDetails: executeWatchDetails.upstreamJson,
@@ -130,6 +148,7 @@ export const executeWatch = async (executeWatchDetails: ExecutedWatchDetails, wa
});
return data;
};
+
export const loadIndexPatterns = async () => {
const { savedObjects } = await getSavedObjectsClient().find({
type: 'index-pattern',
diff --git a/x-pack/plugins/watcher/public/lib/breadcrumbs.js b/x-pack/plugins/watcher/public/lib/breadcrumbs.js
deleted file mode 100644
index c30bc4175375a3..00000000000000
--- a/x-pack/plugins/watcher/public/lib/breadcrumbs.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import chrome from 'ui/chrome';
-import { i18n } from '@kbn/i18n';
-
-import { MANAGEMENT_BREADCRUMB } from 'ui/management';
-
-const uiSettings = chrome.getUiSettingsClient();
-
-export function getWatchListBreadcrumbs() {
- return [
- MANAGEMENT_BREADCRUMB,
- {
- text: i18n.translate('xpack.watcher.list.breadcrumb', {
- defaultMessage: 'Watcher'
- }),
- href: '#/management/elasticsearch/watcher/watches/'
- }
- ];
-}
-
-export function getWatchDetailBreadcrumbs($route) {
- const watch = $route.current.locals.watch || $route.current.locals.xpackWatch;
-
- return [
- ...getWatchListBreadcrumbs(),
- {
- text: !watch.isNew
- ? watch.name
- : i18n.translate('xpack.watcher.create.breadcrumb', { defaultMessage: 'Create' }),
- href: '#/management/elasticsearch/watcher/watches/watch/23eebf28-94fd-47e9-ac44-6fee6e427c33'
- }
- ];
-}
-
-export function getWatchHistoryBreadcrumbs($route) {
- const { watchHistoryItem } = $route.current.locals;
-
- return [
- ...getWatchDetailBreadcrumbs($route),
- {
- text: watchHistoryItem.startTime.format(uiSettings.get('dateFormat'))
- }
- ];
-}
diff --git a/x-pack/plugins/watcher/public/lib/breadcrumbs.ts b/x-pack/plugins/watcher/public/lib/breadcrumbs.ts
new file mode 100644
index 00000000000000..a8c34599557cbd
--- /dev/null
+++ b/x-pack/plugins/watcher/public/lib/breadcrumbs.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const listBreadcrumb: any = {
+ text: i18n.translate('xpack.watcher.breadcrumb.listLabel', {
+ defaultMessage: 'Watcher',
+ }),
+ href: '#/management/elasticsearch/watcher/watches/',
+};
+
+export const createBreadcrumb: any = {
+ text: i18n.translate('xpack.watcher.breadcrumb.createLabel', {
+ defaultMessage: 'Create',
+ }),
+};
+
+export const editBreadcrumb: any = {
+ text: i18n.translate('xpack.watcher.breadcrumb.editLabel', {
+ defaultMessage: 'Edit',
+ }),
+};
+
+export const statusBreadcrumb: any = {
+ text: i18n.translate('xpack.watcher.breadcrumb.statusLabel', {
+ defaultMessage: 'Status',
+ }),
+};
diff --git a/x-pack/plugins/watcher/public/lib/use_request.ts b/x-pack/plugins/watcher/public/lib/use_request.ts
new file mode 100644
index 00000000000000..6770f820b76565
--- /dev/null
+++ b/x-pack/plugins/watcher/public/lib/use_request.ts
@@ -0,0 +1,121 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useEffect, useState } from 'react';
+import { getHttpClient } from './api';
+
+interface SendRequest {
+ path?: string;
+ method: string;
+ body?: any;
+}
+
+interface SendRequestResponse {
+ data: any;
+ error: Error;
+}
+
+export const sendRequest = async ({
+ path,
+ method,
+ body,
+}: SendRequest): Promise> => {
+ try {
+ const response = await (getHttpClient() as any)[method](path, body);
+
+ if (!response.data) {
+ throw new Error(response.statusText);
+ }
+
+ return {
+ data: response.data,
+ };
+ } catch (e) {
+ return {
+ error: e,
+ };
+ }
+};
+
+interface UseRequest extends SendRequest {
+ interval?: number;
+ initialData?: any;
+ processData?: any;
+}
+
+export const useRequest = ({
+ path,
+ method,
+ body,
+ interval,
+ initialData,
+ processData,
+}: UseRequest) => {
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [data, setData] = useState(initialData);
+
+ // Tied to every render and bound to each request.
+ let isOutdatedRequest = false;
+
+ const createRequest = async () => {
+ // Set a neutral state for a non-request.
+ if (!path) {
+ setError(null);
+ setData(initialData);
+ setIsLoading(false);
+ return;
+ }
+
+ setError(null);
+ setData(initialData);
+ setIsLoading(true);
+
+ const { data: responseData, error: responseError } = await sendRequest({
+ path,
+ method,
+ body,
+ });
+
+ // Don't update state if an outdated request has resolved.
+ if (isOutdatedRequest) {
+ return;
+ }
+
+ setError(responseError);
+ setData(processData && responseData ? processData(responseData) : responseData);
+ setIsLoading(false);
+ };
+
+ useEffect(
+ () => {
+ function cancelOutdatedRequest() {
+ isOutdatedRequest = true;
+ }
+
+ createRequest();
+
+ if (interval) {
+ const intervalRequest = setInterval(createRequest, interval);
+ return () => {
+ cancelOutdatedRequest();
+ clearInterval(intervalRequest);
+ };
+ }
+
+ // Called when a new render will trigger this effect.
+ return cancelOutdatedRequest;
+ },
+ [path]
+ );
+
+ return {
+ error,
+ isLoading,
+ data,
+ createRequest,
+ };
+};
diff --git a/x-pack/plugins/watcher/public/register_route.js b/x-pack/plugins/watcher/public/register_route.js
index 997535eced7f19..f4c6b931e9a906 100644
--- a/x-pack/plugins/watcher/public/register_route.js
+++ b/x-pack/plugins/watcher/public/register_route.js
@@ -11,7 +11,6 @@ import { management } from 'ui/management';
import template from './app.html';
import { App } from './app';
import 'plugins/watcher/services/license';
-import { getWatchListBreadcrumbs } from './lib/breadcrumbs';
import { setHttpClient, setSavedObjectsClient } from './lib/api';
import { I18nContext } from 'ui/i18n';
import { manageAngularLifecycle } from './lib/manage_angular_lifecycle';
@@ -49,8 +48,6 @@ routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param
}
},
controllerAs: 'watchRoute',
- //TODO: fix breadcrumbs
- k7Breadcrumbs: getWatchListBreadcrumbs,
});
routes.defaults(/\/management/, {
diff --git a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx
index 46de27ab958389..dd7ba812d40f26 100644
--- a/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx
+++ b/x-pack/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx
@@ -4,14 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLoadingSpinner } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { Watch } from 'plugins/watcher/models/watch';
import React, { useEffect, useReducer } from 'react';
import { isEqual } from 'lodash';
+
+import { EuiLoadingSpinner, EuiPageContent } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import chrome from 'ui/chrome';
+import { MANAGEMENT_BREADCRUMB } from 'ui/management';
+
+import { Watch } from 'plugins/watcher/models/watch';
import { WATCH_TYPES } from '../../../../common/constants';
import { BaseWatch } from '../../../../common/types/watch_types';
+import { getPageErrorCode, PageError } from '../../../components/page_error';
import { loadWatch } from '../../../lib/api';
+import { listBreadcrumb, editBreadcrumb, createBreadcrumb } from '../../../lib/breadcrumbs';
import { JsonWatchEdit } from './json_watch_edit';
import { ThresholdWatchEdit } from './threshold_watch_edit';
import { WatchContext } from './watch_context';
@@ -33,22 +39,45 @@ const getTitle = (watch: BaseWatch) => {
});
}
};
+
const watchReducer = (state: any, action: any) => {
const { command, payload } = action;
+ const { watch } = state;
+
switch (command) {
case 'setWatch':
- return payload;
+ return {
+ ...state,
+ watch: payload,
+ };
+
case 'setProperty':
const { property, value } = payload;
- if (isEqual(state[property], value)) {
+ if (isEqual(watch[property], value)) {
return state;
} else {
- return new (Watch.getWatchTypes())[state.type]({ ...state, [property]: value });
+ return {
+ ...state,
+ watch: new (Watch.getWatchTypes())[watch.type]({
+ ...watch,
+ [property]: value,
+ }),
+ };
}
+
case 'addAction':
- const newWatch = new (Watch.getWatchTypes())[state.type](state);
+ const newWatch = new (Watch.getWatchTypes())[watch.type](watch);
newWatch.addAction(payload);
- return newWatch;
+ return {
+ ...state,
+ watch: newWatch,
+ };
+
+ case 'setError':
+ return {
+ ...state,
+ loadError: payload,
+ };
}
};
@@ -65,17 +94,24 @@ export const WatchEdit = ({
};
}) => {
// hooks
- const [watch, dispatch] = useReducer(watchReducer, null);
+ const [{ watch, loadError }, dispatch] = useReducer(watchReducer, { watch: null });
+
const setWatchProperty = (property: string, value: any) => {
dispatch({ command: 'setProperty', payload: { property, value } });
};
+
const addAction = (action: any) => {
dispatch({ command: 'addAction', payload: action });
};
+
const getWatch = async () => {
if (id) {
- const theWatch = await loadWatch(id);
- dispatch({ command: 'setWatch', payload: theWatch });
+ try {
+ const loadedWatch = await loadWatch(id);
+ dispatch({ command: 'setWatch', payload: loadedWatch });
+ } catch (error) {
+ dispatch({ command: 'setError', payload: error });
+ }
} else if (type) {
const WatchType = Watch.getWatchTypes()[type];
if (WatchType) {
@@ -83,19 +119,45 @@ export const WatchEdit = ({
}
}
};
+
useEffect(() => {
getWatch();
}, []);
+
+ useEffect(
+ () => {
+ chrome.breadcrumbs.set([
+ MANAGEMENT_BREADCRUMB,
+ listBreadcrumb,
+ id ? editBreadcrumb : createBreadcrumb,
+ ]);
+ },
+ [id]
+ );
+
+ const errorCode = getPageErrorCode(loadError);
+ if (errorCode) {
+ return (
+
+
+
+ );
+ }
+
if (!watch) {
return ;
}
+
const pageTitle = getTitle(watch);
+
let EditComponent = null;
+
if (watch.type === WATCH_TYPES.THRESHOLD) {
EditComponent = ThresholdWatchEdit;
} else {
EditComponent = JsonWatchEdit;
}
+
return (
diff --git a/x-pack/plugins/watcher/public/sections/watch_list/components/watch_list.tsx b/x-pack/plugins/watcher/public/sections/watch_list/components/watch_list.tsx
index b61d9a0260b741..57debfda12971f 100644
--- a/x-pack/plugins/watcher/public/sections/watch_list/components/watch_list.tsx
+++ b/x-pack/plugins/watcher/public/sections/watch_list/components/watch_list.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
import {
EuiButton,
@@ -23,9 +23,14 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { Moment } from 'moment';
+import chrome from 'ui/chrome';
+import { MANAGEMENT_BREADCRUMB } from 'ui/management';
+
import { REFRESH_INTERVALS, WATCH_STATES } from '../../../../common/constants';
import { DeleteWatchesModal } from '../../../components/delete_watches_modal';
-import { fetchWatches } from '../../../lib/api';
+import { listBreadcrumb } from '../../../lib/breadcrumbs';
+import { getPageErrorCode, PageError } from '../../../components/page_error';
+import { loadWatches } from '../../../lib/api';
const stateToIcon: { [key: string]: JSX.Element } = {
[WATCH_STATES.OK]: ,
@@ -37,22 +42,34 @@ const stateToIcon: { [key: string]: JSX.Element } = {
const WatchListUi = ({ intl }: { intl: InjectedIntl }) => {
// hooks
- const [isWatchesLoading, setIsWatchesLoading] = useState(true);
- const [watchesToDelete, setWatchesToDelete] = useState([]);
- const [watches, setWatches] = useState([]);
const [selection, setSelection] = useState([]);
- const loadWatches = async () => {
- const loadedWatches = await fetchWatches();
- setWatches(loadedWatches);
- setIsWatchesLoading(false);
- };
+ const [watchesToDelete, setWatchesToDelete] = useState([]);
+ // Filter out deleted watches on the client, because the API will return 200 even though some watches
+ // may not really be deleted until after they're done firing and this could take some time.
+ const [deletedWatches, setDeletedWatches] = useState([]);
+
useEffect(() => {
- loadWatches();
- const refreshIntervalId = setInterval(loadWatches, REFRESH_INTERVALS.WATCH_LIST);
- return () => {
- clearInterval(refreshIntervalId);
- };
+ chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb]);
}, []);
+
+ const { isLoading: isWatchesLoading, data: watches, error } = loadWatches(
+ REFRESH_INTERVALS.WATCH_LIST
+ );
+
+ const availableWatches = useMemo(
+ () =>
+ watches ? watches.filter((watch: any) => !deletedWatches.includes(watch.id)) : undefined,
+ [watches, deletedWatches]
+ );
+
+ if (getPageErrorCode(error)) {
+ return (
+
+
+
+ );
+ }
+
const columns = [
{
field: 'id',
@@ -157,13 +174,16 @@ const WatchListUi = ({ intl }: { intl: InjectedIntl }) => {
],
},
];
+
const selectionConfig = {
onSelectionChange: setSelection,
};
+
const pagination = {
initialPageSize: 10,
pageSizeOptions: [10, 50, 100],
};
+
const searchConfig = {
box: {
incremental: true,
@@ -184,21 +204,19 @@ const WatchListUi = ({ intl }: { intl: InjectedIntl }) => {
),
};
+
return (
{
if (deleted) {
- setWatches(
- watches.filter((watch: any) => {
- return !deleted.includes(watch.id);
- })
- );
+ setDeletedWatches([...deletedWatches, ...watchesToDelete]);
}
setWatchesToDelete([]);
}}
watchesToDelete={watchesToDelete}
/>
+
@@ -209,7 +227,9 @@ const WatchListUi = ({ intl }: { intl: InjectedIntl }) => {
/>
+
+
{
+
+
{
+
+
{
);
};
+
export const WatchList = injectI18n(WatchListUi);
diff --git a/x-pack/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx b/x-pack/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx
index 8d9391a84e8ecb..c517f9652a2093 100644
--- a/x-pack/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx
+++ b/x-pack/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx
@@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import React, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
+
import {
EuiFlexGroup,
EuiFlexItem,
@@ -12,16 +16,11 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
-import React, { Fragment, useEffect, useState } from 'react';
-import { fetchWatchDetail } from '../../../lib/api';
+import { loadWatchDetail } from '../../../lib/api';
+import { getPageErrorCode } from '../../../components/page_error';
import { WatchActionStatus } from './watch_action_status';
const WatchDetailUi = ({ intl, watchId }: { intl: InjectedIntl; watchId: string }) => {
- const [isWatchesLoading, setIsWatchesLoading] = useState(true);
- const [actions, setWatchActions] = useState([]);
-
const pagination = {
initialPageSize: 10,
pageSizeOptions: [10, 50, 100],
@@ -60,15 +59,13 @@ const WatchDetailUi = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
},
},
];
- const loadWatchActions = async () => {
- const loadedWatchActions = await fetchWatchDetail(watchId);
- setWatchActions(loadedWatchActions.watchStatus.actionStatuses);
- setIsWatchesLoading(false);
- };
- useEffect(() => {
- loadWatchActions();
- // only run the first time the component loads
- }, []);
+
+ const { error, data: watchDetail, isLoading } = loadWatchDetail(watchId);
+
+ // Another part of the UI will surface the error.
+ if (getPageErrorCode(error)) {
+ return null;
+ }
return (
@@ -82,12 +79,12 @@ const WatchDetailUi = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
{
- const [isLoading, setIsLoading] = useState(true);
- const [isActivated, setIsActivated] = useState(true);
- const [history, setWatchHistory] = useState([]);
- const [isDetailVisible, setIsDetailVisible] = useState(true);
+const watchHistoryTimeSpanOptions = [
+ {
+ value: 'now-1h',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.1h', {
+ defaultMessage: 'Last one hour',
+ }),
+ },
+ {
+ value: 'now-24h',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.24h', {
+ defaultMessage: 'Last 24 hours',
+ }),
+ },
+ {
+ value: 'now-7d',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.7d', {
+ defaultMessage: 'Last 7 days',
+ }),
+ },
+ {
+ value: 'now-30d',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.30d', {
+ defaultMessage: 'Last 30 days',
+ }),
+ },
+ {
+ value: 'now-6M',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.6M', {
+ defaultMessage: 'Last 6 months',
+ }),
+ },
+ {
+ value: 'now-1y',
+ text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.1y', {
+ defaultMessage: 'Last 1 year',
+ }),
+ },
+];
+
+const WatchHistoryUi = ({ intl, watchId }: { intl: InjectedIntl; watchId: string }) => {
+ const [isActivated, setIsActivated] = useState(undefined);
+ const [detailWatchId, setDetailWatchId] = useState(undefined);
const [watchesToDelete, setWatchesToDelete] = useState([]);
- const [itemDetail, setItemDetail] = useState<{
- id?: string;
- details?: any;
- watchId?: string;
- watchStatus?: { actionStatuses?: any };
- }>({});
- const [executionDetail, setExecutionDetail] = useState('');
- const pagination = {
- initialPageSize: 10,
- pageSizeOptions: [10, 50, 100],
- };
+ const [watchHistoryTimeSpan, setWatchHistoryTimeSpan] = useState(
+ watchHistoryTimeSpanOptions[0].value
+ );
+
+ const { error: watchDetailError, data: loadedWatch } = loadWatchDetail(watchId);
- const loadWatch = async () => {
- const loadedWatch = await fetchWatchDetail(watchId);
+ if (loadedWatch && isActivated === undefined) {
+ // Set initial value for isActivated based on the watch we just loaded.
setIsActivated(loadedWatch.watchStatus.isActive);
- };
+ }
- const watchHistoryTimeSpanOptions = [
- {
- value: 'now-1h',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.1h', {
- defaultMessage: 'Last one hour',
- }),
- },
- {
- value: 'now-24h',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.24h', {
- defaultMessage: 'Last 24 hours',
- }),
- },
- {
- value: 'now-7d',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.7d', {
- defaultMessage: 'Last 7 days',
- }),
- },
- {
- value: 'now-30d',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.30d', {
- defaultMessage: 'Last 30 days',
- }),
- },
- {
- value: 'now-6M',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.6M', {
- defaultMessage: 'Last 6 months',
- }),
- },
- {
- value: 'now-1y',
- text: i18n.translate('xpack.watcher.sections.watchHistory.timeSpan.1y', {
- defaultMessage: 'Last 1 year',
- }),
- },
- ];
- const [watchHistoryTimeSpan, setWatchHistoryTimeSpan] = useState(
- watchHistoryTimeSpanOptions[0].value
+ const { error: historyError, data: history, isLoading } = loadWatchHistory(
+ watchId,
+ watchHistoryTimeSpan
+ );
+
+ const { error: watchHistoryDetailsError, data: watchHistoryDetails } = loadWatchHistoryDetail(
+ detailWatchId
);
+ const executionDetail = watchHistoryDetails
+ ? JSON.stringify(watchHistoryDetails.details, null, 2)
+ : '';
+
+ const errorCode = getPageErrorCode([watchDetailError, historyError, watchHistoryDetailsError]);
+ if (errorCode) {
+ return ;
+ }
+
+ const pagination = {
+ initialPageSize: 10,
+ pageSizeOptions: [10, 50, 100],
+ };
+
const columns = [
{
field: 'startTime',
@@ -114,7 +129,7 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
showDetailFlyout(item)}
+ onClick={() => setDetailWatchId(item.id)}
>
{formattedDate}
@@ -157,24 +172,6 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
const onTimespanChange = (e: React.ChangeEvent) => {
const timespan = e.target.value;
setWatchHistoryTimeSpan(timespan);
- loadWatchHistory(timespan);
- };
- const loadWatchHistory = async (timespan: string) => {
- const loadedWatchHistory = await fetchWatchHistory(watchId, timespan);
- setWatchHistory(loadedWatchHistory);
- setIsLoading(false);
- };
-
- const hideDetailFlyout = async () => {
- setItemDetail({});
- return setIsDetailVisible(false);
- };
-
- const showDetailFlyout = async (item: { id: string }) => {
- const watchHistoryItemDetail = await fetchWatchHistoryDetail(item.id);
- setItemDetail(watchHistoryItemDetail);
- setExecutionDetail(JSON.stringify(watchHistoryItemDetail.details, null, 2));
- return setIsDetailVisible(true);
};
const toggleWatchActivation = async () => {
@@ -184,6 +181,7 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
} else {
await activateWatch(watchId);
}
+
setIsActivated(!isActivated);
} catch (e) {
if (e.data.statusCode !== 200) {
@@ -199,15 +197,9 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
}
};
- useEffect(() => {
- loadWatchHistory(watchHistoryTimeSpan);
- loadWatch();
- // only run the first time the component loads
- }, []);
-
let flyout;
- if (isDetailVisible && Object.keys(itemDetail).length !== 0) {
+ if (detailWatchId !== undefined && watchHistoryDetails !== undefined) {
const detailColumns = [
{
field: 'id',
@@ -236,7 +228,7 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
flyout = (
setDetailWatchId(undefined)}
aria-labelledby="indexDetailsFlyoutTitle"
>
@@ -247,7 +239,7 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
);
}
- const activationButtonText = isActivated ? 'Deactivate watch' : 'Activate watch';
+
+ const activationButtonText = isActivated ? (
+
+ ) : (
+
+ );
+
return (
@@ -343,4 +352,4 @@ const WatchHistoryUI = ({ intl, watchId }: { intl: InjectedIntl; watchId: string
);
};
-export const WatchHistory = injectI18n(WatchHistoryUI);
+export const WatchHistory = injectI18n(WatchHistoryUi);
diff --git a/x-pack/plugins/watcher/public/sections/watch_status/watch_status.tsx b/x-pack/plugins/watcher/public/sections/watch_status/watch_status.tsx
index 7ddde3cddead57..fdd853e3498fc2 100644
--- a/x-pack/plugins/watcher/public/sections/watch_status/watch_status.tsx
+++ b/x-pack/plugins/watcher/public/sections/watch_status/watch_status.tsx
@@ -4,10 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { EuiPageContent, EuiSpacer } from '@elastic/eui';
+import chrome from 'ui/chrome';
+import { MANAGEMENT_BREADCRUMB } from 'ui/management';
+
import { WatchDetail } from './components/watch_detail';
import { WatchHistory } from './components/watch_history';
+import { listBreadcrumb, statusBreadcrumb } from '../../lib/breadcrumbs';
export const WatchStatus = ({
match: {
@@ -20,6 +24,13 @@ export const WatchStatus = ({
};
};
}) => {
+ useEffect(
+ () => {
+ chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]);
+ },
+ [id]
+ );
+
return (