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 (