mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Metrics UI] UX improvements for saved views (#69910)
* Works-ish * Load the default view without throwing error * Design feedback * Update Saved Views design on Metrics explorer * Fix types * UX improvements when saving and editng * Only load default view if there is no state from anywhere else. * Add loading indicator and other polish * Hide saved view menu when opening modals * Fix typecheck * Fix typo * Fix translations
This commit is contained in:
parent
3f44757973
commit
470397075f
36 changed files with 1023 additions and 215 deletions
|
@ -329,6 +329,10 @@ export interface UpdateSourceInput {
|
|||
logAlias?: string | null;
|
||||
/** The field mapping to use for this source */
|
||||
fields?: UpdateSourceFieldsInput | null;
|
||||
/** Default view for inventory */
|
||||
inventoryDefaultView?: string | null;
|
||||
/** Default view for Metrics Explorer */
|
||||
metricsExplorerDefaultView?: string | null;
|
||||
/** The log columns to display for this source */
|
||||
logColumns?: UpdateSourceLogColumnInput[] | null;
|
||||
}
|
||||
|
@ -875,6 +879,10 @@ export namespace SourceConfigurationFields {
|
|||
fields: Fields;
|
||||
|
||||
logColumns: LogColumns[];
|
||||
|
||||
inventoryDefaultView: string;
|
||||
|
||||
metricsExplorerDefaultView: string;
|
||||
};
|
||||
|
||||
export type Fields = {
|
||||
|
|
|
@ -69,6 +69,8 @@ export const SavedSourceConfigurationRuntimeType = rt.partial({
|
|||
description: rt.string,
|
||||
metricAlias: rt.string,
|
||||
logAlias: rt.string,
|
||||
inventoryDefaultView: rt.string,
|
||||
metricsExplorerDefaultView: rt.string,
|
||||
fields: SavedSourceConfigurationFieldsRuntimeType,
|
||||
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
|
||||
});
|
||||
|
@ -79,7 +81,16 @@ export interface InfraSavedSourceConfiguration
|
|||
export const pickSavedSourceConfiguration = (
|
||||
value: InfraSourceConfiguration
|
||||
): InfraSavedSourceConfiguration => {
|
||||
const { name, description, metricAlias, logAlias, fields, logColumns } = value;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
metricAlias,
|
||||
logAlias,
|
||||
fields,
|
||||
inventoryDefaultView,
|
||||
metricsExplorerDefaultView,
|
||||
logColumns,
|
||||
} = value;
|
||||
const { container, host, pod, tiebreaker, timestamp } = fields;
|
||||
|
||||
return {
|
||||
|
@ -87,6 +98,8 @@ export const pickSavedSourceConfiguration = (
|
|||
description,
|
||||
metricAlias,
|
||||
logAlias,
|
||||
inventoryDefaultView,
|
||||
metricsExplorerDefaultView,
|
||||
fields: { container, host, pod, tiebreaker, timestamp },
|
||||
logColumns,
|
||||
};
|
||||
|
@ -106,6 +119,8 @@ export const StaticSourceConfigurationRuntimeType = rt.partial({
|
|||
description: rt.string,
|
||||
metricAlias: rt.string,
|
||||
logAlias: rt.string,
|
||||
inventoryDefaultView: rt.string,
|
||||
metricsExplorerDefaultView: rt.string,
|
||||
fields: StaticSourceConfigurationFieldsRuntimeType,
|
||||
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
|
||||
});
|
||||
|
|
|
@ -55,6 +55,9 @@ export const metricsExplorerViewSavedObjectType: SavedObjectsType = {
|
|||
aggregation: {
|
||||
type: 'keyword',
|
||||
},
|
||||
source: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
chartOptions: {
|
||||
|
|
|
@ -63,6 +63,8 @@ describe('ExpressionChart', () => {
|
|||
logColumns: [],
|
||||
metricAlias: 'metricbeat-*',
|
||||
logAlias: 'filebeat-*',
|
||||
inventoryDefaultView: 'host',
|
||||
metricsExplorerDefaultView: 'host',
|
||||
fields: {
|
||||
timestamp: '@timestamp',
|
||||
message: ['message'],
|
||||
|
|
|
@ -19,17 +19,21 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { SavedView } from '../../hooks/use_saved_view';
|
||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||
|
||||
interface Props<ViewState> {
|
||||
views: Array<SavedView<ViewState>>;
|
||||
loading: boolean;
|
||||
defaultViewId: string;
|
||||
sourceIsLoading: boolean;
|
||||
close(): void;
|
||||
makeDefault(id: string): void;
|
||||
setView(viewState: ViewState): void;
|
||||
deleteView(id: string): void;
|
||||
}
|
||||
|
||||
interface DeleteConfimationProps {
|
||||
isDisabled?: boolean;
|
||||
confirmedAction(): void;
|
||||
}
|
||||
const DeleteConfimation = (props: DeleteConfimationProps) => {
|
||||
|
@ -46,6 +50,7 @@ const DeleteConfimation = (props: DeleteConfimationProps) => {
|
|||
<FormattedMessage defaultMessage="cancel" id="xpack.infra.waffle.savedViews.cancel" />
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
disabled={props.isDisabled}
|
||||
fill={true}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
|
@ -64,13 +69,17 @@ const DeleteConfimation = (props: DeleteConfimationProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export function SavedViewListFlyout<ViewState>({
|
||||
export function SavedViewManageViewsFlyout<ViewState>({
|
||||
close,
|
||||
views,
|
||||
defaultViewId,
|
||||
setView,
|
||||
makeDefault,
|
||||
deleteView,
|
||||
loading,
|
||||
sourceIsLoading,
|
||||
}: Props<ViewState>) {
|
||||
const [inProgressView, setInProgressView] = useState<string | null>(null);
|
||||
const renderName = useCallback(
|
||||
(name: string, item: SavedView<ViewState>) => (
|
||||
<EuiButtonEmpty
|
||||
|
@ -89,6 +98,7 @@ export function SavedViewListFlyout<ViewState>({
|
|||
(item: SavedView<ViewState>) => {
|
||||
return (
|
||||
<DeleteConfimation
|
||||
isDisabled={item.isDefault}
|
||||
confirmedAction={() => {
|
||||
deleteView(item.id);
|
||||
}}
|
||||
|
@ -98,6 +108,25 @@ export function SavedViewListFlyout<ViewState>({
|
|||
[deleteView]
|
||||
);
|
||||
|
||||
const renderMakeDefaultAction = useCallback(
|
||||
(item: SavedView<ViewState>) => {
|
||||
const isDefault = item.id === defaultViewId;
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
isLoading={inProgressView === item.id && sourceIsLoading}
|
||||
iconType={isDefault ? 'starFilled' : 'starEmpty'}
|
||||
onClick={() => {
|
||||
setInProgressView(item.id);
|
||||
makeDefault(item.id);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[makeDefault, defaultViewId, sourceIsLoading, inProgressView]
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
|
@ -112,7 +141,11 @@ export function SavedViewListFlyout<ViewState>({
|
|||
}),
|
||||
actions: [
|
||||
{
|
||||
available: (item: SavedView<ViewState>) => !item.isDefault,
|
||||
available: () => true,
|
||||
render: renderMakeDefaultAction,
|
||||
},
|
||||
{
|
||||
available: (item: SavedView<ViewState>) => true,
|
||||
render: renderDeleteAction,
|
||||
},
|
||||
],
|
||||
|
@ -124,7 +157,10 @@ export function SavedViewListFlyout<ViewState>({
|
|||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Load views" id="xpack.infra.openView.flyoutHeader" />
|
||||
<FormattedMessage
|
||||
defaultMessage="Manage saved views"
|
||||
id="xpack.infra.openView.flyoutHeader"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
|
@ -4,20 +4,28 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { useCallback, useState, useEffect, useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useSavedView } from '../../hooks/use_saved_view';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { EuiPopover } from '@elastic/eui';
|
||||
import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { SavedViewCreateModal } from './create_modal';
|
||||
import { SavedViewListFlyout } from './view_list_flyout';
|
||||
import { SavedViewUpdateModal } from './update_modal';
|
||||
import { SavedViewManageViewsFlyout } from './manage_views_flyout';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||
import { SavedViewListModal } from './view_list_modal';
|
||||
|
||||
interface Props<ViewState> {
|
||||
viewType: string;
|
||||
viewState: ViewState;
|
||||
defaultViewState: ViewState;
|
||||
onViewChange(viewState: ViewState): void;
|
||||
}
|
||||
|
||||
export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
|
||||
|
@ -26,37 +34,80 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
|
|||
views,
|
||||
saveView,
|
||||
loading,
|
||||
updateView,
|
||||
deletedId,
|
||||
deleteView,
|
||||
defaultViewId,
|
||||
makeDefault,
|
||||
sourceIsLoading,
|
||||
find,
|
||||
errorOnFind,
|
||||
errorOnCreate,
|
||||
createdId,
|
||||
} = useSavedView(props.defaultViewState, props.viewType);
|
||||
createdView,
|
||||
updatedView,
|
||||
currentView,
|
||||
setCurrentView,
|
||||
} = useContext(SavedView.Context);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [viewListModalOpen, setViewListModalOpen] = useState(false);
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [isSavedViewMenuOpen, setIsSavedViewMenuOpen] = useState(false);
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [updateModalOpen, setUpdateModalOpen] = useState(false);
|
||||
const hideSavedViewMenu = useCallback(() => {
|
||||
setIsSavedViewMenuOpen(false);
|
||||
}, [setIsSavedViewMenuOpen]);
|
||||
const openViewListModal = useCallback(() => {
|
||||
hideSavedViewMenu();
|
||||
find();
|
||||
setViewListModalOpen(true);
|
||||
}, [setViewListModalOpen, find, hideSavedViewMenu]);
|
||||
const closeViewListModal = useCallback(() => {
|
||||
setViewListModalOpen(false);
|
||||
}, [setViewListModalOpen]);
|
||||
const openSaveModal = useCallback(() => {
|
||||
hideSavedViewMenu();
|
||||
setIsInvalid(false);
|
||||
setCreateModalOpen(true);
|
||||
}, []);
|
||||
}, [hideSavedViewMenu]);
|
||||
const openUpdateModal = useCallback(() => {
|
||||
hideSavedViewMenu();
|
||||
setIsInvalid(false);
|
||||
setUpdateModalOpen(true);
|
||||
}, [hideSavedViewMenu]);
|
||||
const closeModal = useCallback(() => setModalOpen(false), []);
|
||||
const closeCreateModal = useCallback(() => setCreateModalOpen(false), []);
|
||||
const closeUpdateModal = useCallback(() => setUpdateModalOpen(false), []);
|
||||
const loadViews = useCallback(() => {
|
||||
hideSavedViewMenu();
|
||||
find();
|
||||
setModalOpen(true);
|
||||
}, [find]);
|
||||
}, [find, hideSavedViewMenu]);
|
||||
const showSavedViewMenu = useCallback(() => {
|
||||
setIsSavedViewMenuOpen(true);
|
||||
}, [setIsSavedViewMenuOpen]);
|
||||
const save = useCallback(
|
||||
(name: string, hasTime: boolean = false) => {
|
||||
const currentState = {
|
||||
...props.viewState,
|
||||
...(!hasTime ? { time: undefined } : {}),
|
||||
};
|
||||
saveView({ name, ...currentState });
|
||||
saveView({ ...currentState, name });
|
||||
},
|
||||
[props.viewState, saveView]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
(name: string, hasTime: boolean = false) => {
|
||||
const currentState = {
|
||||
...props.viewState,
|
||||
...(!hasTime ? { time: undefined } : {}),
|
||||
};
|
||||
updateView(currentView.id, { ...currentState, name });
|
||||
},
|
||||
[props.viewState, updateView, currentView]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorOnCreate) {
|
||||
setIsInvalid(true);
|
||||
|
@ -64,11 +115,20 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
|
|||
}, [errorOnCreate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (createdId !== undefined) {
|
||||
if (updatedView !== undefined) {
|
||||
setCurrentView(updatedView);
|
||||
// INFO: Close the modal after the view is created.
|
||||
closeUpdateModal();
|
||||
}
|
||||
}, [updatedView, setCurrentView, closeUpdateModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (createdView !== undefined) {
|
||||
// INFO: Close the modal after the view is created.
|
||||
setCurrentView(createdView);
|
||||
closeCreateModal();
|
||||
}
|
||||
}, [createdId, closeCreateModal]);
|
||||
}, [createdView, setCurrentView, closeCreateModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (deletedId !== undefined) {
|
||||
|
@ -88,30 +148,110 @@ export function SavedViewsToolbarControls<ViewState>(props: Props<ViewState>) {
|
|||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiButtonEmpty iconType="save" onClick={openSaveModal} data-test-subj="openSaveViewModal">
|
||||
<FormattedMessage
|
||||
defaultMessage="Save"
|
||||
id="xpack.infra.waffle.savedViews.saveViewLabel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty iconType="importAction" onClick={loadViews} data-test-subj="loadViews">
|
||||
<FormattedMessage
|
||||
defaultMessage="Load"
|
||||
id="xpack.infra.waffle.savedViews.loadViewsLabel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.infra.savedView.changeView', {
|
||||
defaultMessage: 'Change view',
|
||||
})}
|
||||
onClick={showSavedViewMenu}
|
||||
iconType="globe"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList onClick={showSavedViewMenu}>
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Current view"
|
||||
id="xpack.infra.savedView.currentView"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{currentView
|
||||
? currentView.name
|
||||
: i18n.translate('xpack.infra.savedView.unknownView', {
|
||||
defaultMessage: 'Unknown',
|
||||
})}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
isOpen={isSavedViewMenuOpen}
|
||||
closePopover={hideSavedViewMenu}
|
||||
anchorPosition="upCenter"
|
||||
>
|
||||
<EuiListGroup flush={true}>
|
||||
<EuiListGroupItem
|
||||
iconType={'indexSettings'}
|
||||
onClick={loadViews}
|
||||
label={i18n.translate('xpack.infra.savedView.manageViews', {
|
||||
defaultMessage: 'Manage views',
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiListGroupItem
|
||||
iconType={'refresh'}
|
||||
onClick={openUpdateModal}
|
||||
disabled={!currentView || currentView.id === '0'}
|
||||
label={i18n.translate('xpack.infra.savedView.updateView', {
|
||||
defaultMessage: 'Update view',
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiListGroupItem
|
||||
iconType={'importAction'}
|
||||
onClick={openViewListModal}
|
||||
label={i18n.translate('xpack.infra.savedView.loadView', {
|
||||
defaultMessage: 'Load view',
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiListGroupItem
|
||||
iconType={'save'}
|
||||
onClick={openSaveModal}
|
||||
label={i18n.translate('xpack.infra.savedView.saveNewView', {
|
||||
defaultMessage: 'Save new view',
|
||||
})}
|
||||
/>
|
||||
</EuiListGroup>
|
||||
</EuiPopover>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{createModalOpen && (
|
||||
<SavedViewCreateModal isInvalid={isInvalid} close={closeCreateModal} save={save} />
|
||||
)}
|
||||
|
||||
{updateModalOpen && (
|
||||
<SavedViewUpdateModal
|
||||
currentView={currentView}
|
||||
isInvalid={isInvalid}
|
||||
close={closeUpdateModal}
|
||||
save={update}
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewListModalOpen && (
|
||||
<SavedViewListModal<any>
|
||||
currentView={currentView}
|
||||
views={views}
|
||||
close={closeViewListModal}
|
||||
setView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalOpen && (
|
||||
<SavedViewListFlyout<ViewState>
|
||||
<SavedViewManageViewsFlyout<ViewState>
|
||||
sourceIsLoading={sourceIsLoading}
|
||||
loading={loading}
|
||||
views={views}
|
||||
defaultViewId={defaultViewId}
|
||||
makeDefault={makeDefault}
|
||||
deleteView={deleteView}
|
||||
close={closeModal}
|
||||
setView={props.onViewChange}
|
||||
setView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
EuiFieldText,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props<ViewState> {
|
||||
isInvalid: boolean;
|
||||
close(): void;
|
||||
save(name: string, shouldIncludeTime: boolean): void;
|
||||
currentView: ViewState;
|
||||
}
|
||||
|
||||
export function SavedViewUpdateModal<ViewState extends { id: string; name: string }>({
|
||||
close,
|
||||
save,
|
||||
isInvalid,
|
||||
currentView,
|
||||
}: Props<ViewState>) {
|
||||
const [viewName, setViewName] = useState(currentView.name);
|
||||
const [includeTime, setIncludeTime] = useState(false);
|
||||
const onCheckChange = useCallback((e) => setIncludeTime(e.target.checked), []);
|
||||
const textChange = useCallback((e) => setViewName(e.target.value), []);
|
||||
|
||||
const saveView = useCallback(() => {
|
||||
save(viewName, includeTime);
|
||||
}, [includeTime, save, viewName]);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={close}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Update View"
|
||||
id="xpack.infra.waffle.savedView.updateHeader"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiFieldText
|
||||
isInvalid={isInvalid}
|
||||
placeholder={i18n.translate('xpack.infra.waffle.savedViews.viewNamePlaceholder', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
data-test-subj="savedViewViweName"
|
||||
value={viewName}
|
||||
onChange={textChange}
|
||||
aria-label={i18n.translate('xpack.infra.waffle.savedViews.viewNamePlaceholder', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiSwitch
|
||||
id={'saved-view-save-time-checkbox'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Store time with view"
|
||||
id="xpack.infra.waffle.savedViews.includeTimeFilterLabel"
|
||||
/>
|
||||
}
|
||||
checked={includeTime}
|
||||
onChange={onCheckChange}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size={'xs'} grow={false} style={{ maxWidth: 400 }}>
|
||||
<FormattedMessage
|
||||
defaultMessage="This changes the time filter to the currently selected time each time the view is loaded"
|
||||
id="xpack.infra.waffle.savedViews.includeTimeHelpText"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={close}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="xpack.infra.waffle.savedViews.cancelButton"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
disabled={!viewName}
|
||||
fill={true}
|
||||
onClick={saveView}
|
||||
data-test-subj="updateSavedViewButton"
|
||||
>
|
||||
<FormattedMessage defaultMessage="Save" id="xpack.infra.waffle.savedViews.saveButton" />
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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, { useCallback, useState, useMemo } from 'react';
|
||||
|
||||
import { EuiButtonEmpty, EuiModalFooter, EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiOverlayMask,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
} from '@elastic/eui';
|
||||
import { EuiSelectable } from '@elastic/eui';
|
||||
import { EuiSelectableOption } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||
|
||||
interface Props<ViewState> {
|
||||
views: Array<SavedView<ViewState>>;
|
||||
close(): void;
|
||||
setView(viewState: ViewState): void;
|
||||
currentView?: ViewState;
|
||||
}
|
||||
|
||||
export function SavedViewListModal<ViewState extends { id: string; name: string }>({
|
||||
close,
|
||||
views,
|
||||
setView,
|
||||
currentView,
|
||||
}: Props<ViewState>) {
|
||||
const [options, setOptions] = useState<EuiSelectableOption[] | null>(null);
|
||||
|
||||
const onChange = useCallback((opts: EuiSelectableOption[]) => {
|
||||
setOptions(opts);
|
||||
}, []);
|
||||
|
||||
const loadView = useCallback(() => {
|
||||
if (!options) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = options.find((o) => o.checked);
|
||||
if (!selected) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setView(views.find((v) => v.id === selected.key)!);
|
||||
close();
|
||||
}, [options, views, setView, close]);
|
||||
|
||||
const defaultOptions = useMemo<EuiSelectableOption[]>(() => {
|
||||
return views.map((v) => ({
|
||||
label: v.name,
|
||||
key: v.id,
|
||||
checked: currentView?.id === v.id ? 'on' : undefined,
|
||||
}));
|
||||
}, [views, currentView]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={close}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a view to load"
|
||||
id="xpack.infra.waffle.savedView.selectViewHeader"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiSelectable
|
||||
singleSelection={true}
|
||||
searchable={true}
|
||||
options={options || defaultOptions}
|
||||
onChange={onChange}
|
||||
searchProps={{
|
||||
placeholder: i18n.translate('xpack.infra.savedView.searchPlaceholder', {
|
||||
defaultMessage: 'Search for saved views',
|
||||
}),
|
||||
}}
|
||||
listProps={{ bordered: true }}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
{search}
|
||||
<div style={{ marginTop: 20 }}>{list}</div>
|
||||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty data-test-subj="cancelSavedViewModal" onClick={close}>
|
||||
<FormattedMessage defaultMessage="Cancel" id="xpack.infra.openView.cancelButton" />
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
color={'primary'}
|
||||
data-test-subj="loadSavedViewModal"
|
||||
onClick={loadView}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Load view" id="xpack.infra.openView.loadButton" />
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
|
@ -97,6 +97,7 @@ function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOption
|
|||
limit: t.number,
|
||||
groupBy: t.string,
|
||||
filterQuery: t.string,
|
||||
source: t.string,
|
||||
});
|
||||
|
||||
const Options = t.intersection([OptionsRequired, OptionsOptional]);
|
||||
|
@ -156,6 +157,7 @@ const mapToUrlState = (value: any): MetricsExplorerUrlState | undefined => {
|
|||
const finalState = {};
|
||||
if (value) {
|
||||
if (value.options && isMetricExplorerOptions(value.options)) {
|
||||
value.options.source = 'url';
|
||||
set(finalState, 'options', value.options);
|
||||
}
|
||||
if (value.timerange && isMetricExplorerTimeOption(value.timerange)) {
|
||||
|
|
262
x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx
Normal file
262
x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* 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 createContainer from 'constate';
|
||||
import { useCallback, useMemo, useState, useEffect, useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SimpleSavedObject, SavedObjectAttributes } from 'kibana/public';
|
||||
import { useFindSavedObject } from '../../hooks/use_find_saved_object';
|
||||
import { useCreateSavedObject } from '../../hooks/use_create_saved_object';
|
||||
import { useDeleteSavedObject } from '../../hooks/use_delete_saved_object';
|
||||
import { Source } from '../source';
|
||||
import { metricsExplorerViewSavedObjectName } from '../../../common/saved_objects/metrics_explorer_view';
|
||||
import { inventoryViewSavedObjectName } from '../../../common/saved_objects/inventory_view';
|
||||
import { useSourceConfigurationFormState } from '../../components/source_configuration/source_configuration_form_state';
|
||||
import { useGetSavedObject } from '../../hooks/use_get_saved_object';
|
||||
import { useUpdateSavedObject } from '../../hooks/use_update_saved_object';
|
||||
|
||||
export type SavedView<ViewState> = ViewState & {
|
||||
name: string;
|
||||
id: string;
|
||||
isDefault?: boolean;
|
||||
};
|
||||
|
||||
export type SavedViewSavedObject<ViewState = {}> = ViewState & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ViewType =
|
||||
| typeof metricsExplorerViewSavedObjectName
|
||||
| typeof inventoryViewSavedObjectName;
|
||||
|
||||
interface Props {
|
||||
defaultViewState: SavedView<any>;
|
||||
viewType: ViewType;
|
||||
shouldLoadDefault: boolean;
|
||||
}
|
||||
|
||||
export const useSavedView = (props: Props) => {
|
||||
const {
|
||||
source,
|
||||
isLoading: sourceIsLoading,
|
||||
sourceExists,
|
||||
createSourceConfiguration,
|
||||
updateSourceConfiguration,
|
||||
} = useContext(Source.Context);
|
||||
const { viewType, defaultViewState } = props;
|
||||
type ViewState = typeof defaultViewState;
|
||||
const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject<
|
||||
SavedViewSavedObject<ViewState>
|
||||
>(viewType);
|
||||
|
||||
const [currentView, setCurrentView] = useState<SavedView<any> | null>(null);
|
||||
const [loadingDefaultView, setLoadingDefaultView] = useState<boolean | null>(null);
|
||||
const { create, error: errorOnCreate, data: createdViewData, createdId } = useCreateSavedObject(
|
||||
viewType
|
||||
);
|
||||
const { update, error: errorOnUpdate, data: updatedViewData, updatedId } = useUpdateSavedObject(
|
||||
viewType
|
||||
);
|
||||
const { deleteObject, deletedId } = useDeleteSavedObject(viewType);
|
||||
const { getObject, data: currentViewSavedObject } = useGetSavedObject(viewType);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => setCreateError(errorOnCreate), [errorOnCreate]);
|
||||
|
||||
const deleteView = useCallback((id: string) => deleteObject(id), [deleteObject]);
|
||||
const formState = useSourceConfigurationFormState(source && source.configuration);
|
||||
const defaultViewFieldName = useMemo(
|
||||
() => (viewType === 'inventory-view' ? 'inventoryDefaultView' : 'metricsExplorerDefaultView'),
|
||||
[viewType]
|
||||
);
|
||||
|
||||
const makeDefault = useCallback(
|
||||
async (id: string) => {
|
||||
if (sourceExists) {
|
||||
await updateSourceConfiguration({
|
||||
...formState.formStateChanges,
|
||||
[defaultViewFieldName]: id,
|
||||
});
|
||||
} else {
|
||||
await createSourceConfiguration({
|
||||
...formState.formState,
|
||||
[defaultViewFieldName]: id,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
formState.formState,
|
||||
formState.formStateChanges,
|
||||
sourceExists,
|
||||
defaultViewFieldName,
|
||||
createSourceConfiguration,
|
||||
updateSourceConfiguration,
|
||||
]
|
||||
);
|
||||
|
||||
const saveView = useCallback(
|
||||
(d: { [p: string]: any }) => {
|
||||
const doSave = async () => {
|
||||
const exists = await hasView(d.name);
|
||||
if (exists) {
|
||||
setCreateError(
|
||||
i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', {
|
||||
defaultMessage: `A view with that name already exists.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
create(d);
|
||||
};
|
||||
setCreateError(null);
|
||||
doSave();
|
||||
},
|
||||
[create, hasView]
|
||||
);
|
||||
|
||||
const updateView = useCallback(
|
||||
(id, d: { [p: string]: any }) => {
|
||||
const doSave = async () => {
|
||||
const view = await hasView(d.name);
|
||||
if (view && view.id !== id) {
|
||||
setCreateError(
|
||||
i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', {
|
||||
defaultMessage: `A view with that name already exists.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
update(id, d);
|
||||
};
|
||||
setCreateError(null);
|
||||
doSave();
|
||||
},
|
||||
[update, hasView]
|
||||
);
|
||||
|
||||
const defaultViewId = useMemo(() => {
|
||||
if (!source || !source.configuration) {
|
||||
return '';
|
||||
}
|
||||
if (defaultViewFieldName === 'inventoryDefaultView') {
|
||||
return source.configuration.inventoryDefaultView;
|
||||
} else if (defaultViewFieldName === 'metricsExplorerDefaultView') {
|
||||
return source.configuration.metricsExplorerDefaultView;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}, [source, defaultViewFieldName]);
|
||||
|
||||
const mapToView = useCallback(
|
||||
(o: SimpleSavedObject<SavedObjectAttributes>) => {
|
||||
return {
|
||||
...o.attributes,
|
||||
id: o.id,
|
||||
isDefault: defaultViewId === o.id,
|
||||
};
|
||||
},
|
||||
[defaultViewId]
|
||||
);
|
||||
|
||||
const savedObjects = useMemo(() => (data ? data.savedObjects : []), [data]);
|
||||
|
||||
const views = useMemo(() => {
|
||||
const items: Array<SavedView<ViewState>> = [
|
||||
{
|
||||
name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', {
|
||||
defaultMessage: 'Default view',
|
||||
}),
|
||||
id: '0',
|
||||
isDefault: !defaultViewId || defaultViewId === '0', // If there is no default view then hosts is the default
|
||||
...defaultViewState,
|
||||
},
|
||||
];
|
||||
|
||||
savedObjects.forEach((o) => o.type === viewType && items.push(mapToView(o)));
|
||||
|
||||
return items;
|
||||
}, [defaultViewState, savedObjects, viewType, defaultViewId, mapToView]);
|
||||
|
||||
const createdView = useMemo(() => {
|
||||
return createdViewData ? mapToView(createdViewData) : null;
|
||||
}, [createdViewData, mapToView]);
|
||||
|
||||
const updatedView = useMemo(() => {
|
||||
return updatedViewData ? mapToView(updatedViewData) : null;
|
||||
}, [updatedViewData, mapToView]);
|
||||
|
||||
const loadDefaultView = useCallback(() => {
|
||||
setLoadingDefaultView(true);
|
||||
getObject(defaultViewId);
|
||||
}, [setLoadingDefaultView, getObject, defaultViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentViewSavedObject) {
|
||||
setCurrentView(mapToView(currentViewSavedObject));
|
||||
setLoadingDefaultView(false);
|
||||
}
|
||||
}, [currentViewSavedObject, defaultViewId, mapToView]);
|
||||
|
||||
const setDefault = useCallback(() => {
|
||||
setCurrentView({
|
||||
name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', {
|
||||
defaultMessage: 'Default view',
|
||||
}),
|
||||
id: '0',
|
||||
isDefault: !defaultViewId || defaultViewId === '0', // If there is no default view then hosts is the default
|
||||
...defaultViewState,
|
||||
});
|
||||
}, [setCurrentView, defaultViewId, defaultViewState]);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldLoadDefault = props.shouldLoadDefault;
|
||||
|
||||
if (loadingDefaultView || currentView || !shouldLoadDefault) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultViewId !== '0') {
|
||||
loadDefaultView();
|
||||
} else {
|
||||
setDefault();
|
||||
setLoadingDefaultView(false);
|
||||
}
|
||||
}, [
|
||||
loadDefaultView,
|
||||
props.shouldLoadDefault,
|
||||
setDefault,
|
||||
loadingDefaultView,
|
||||
currentView,
|
||||
defaultViewId,
|
||||
]);
|
||||
|
||||
return {
|
||||
views,
|
||||
saveView,
|
||||
defaultViewId,
|
||||
loading,
|
||||
updateView,
|
||||
updatedView,
|
||||
updatedId,
|
||||
deletedId,
|
||||
createdId,
|
||||
createdView,
|
||||
errorOnUpdate,
|
||||
errorOnFind,
|
||||
errorOnCreate: createError,
|
||||
shouldLoadDefault: props.shouldLoadDefault,
|
||||
makeDefault,
|
||||
sourceIsLoading,
|
||||
deleteView,
|
||||
loadingDefaultView,
|
||||
setCurrentView,
|
||||
currentView,
|
||||
loadDefaultView,
|
||||
find,
|
||||
};
|
||||
};
|
||||
|
||||
export const SavedView = createContainer(useSavedView);
|
||||
export const [SavedViewProvider, useSavedViewContext] = SavedView;
|
|
@ -12,6 +12,8 @@ export const sourceConfigurationFieldsFragment = gql`
|
|||
description
|
||||
logAlias
|
||||
metricAlias
|
||||
inventoryDefaultView
|
||||
metricsExplorerDefaultView
|
||||
fields {
|
||||
container
|
||||
host
|
||||
|
|
|
@ -54,6 +54,10 @@ export interface InfraSourceConfiguration {
|
|||
logAlias: string;
|
||||
/** The field mapping to use for this source */
|
||||
fields: InfraSourceFields;
|
||||
/** Default view for inventory */
|
||||
inventoryDefaultView: string;
|
||||
/** Default view for Metrics Explorer */
|
||||
metricsExplorerDefaultView?: string | null;
|
||||
/** The columns to use for log display */
|
||||
logColumns: InfraSourceLogColumn[];
|
||||
}
|
||||
|
@ -331,6 +335,10 @@ export interface UpdateSourceInput {
|
|||
logAlias?: string | null;
|
||||
/** The field mapping to use for this source */
|
||||
fields?: UpdateSourceFieldsInput | null;
|
||||
/** Name of default inventory view */
|
||||
inventoryDefaultView?: string | null;
|
||||
/** Default view for Metrics Explorer */
|
||||
metricsExplorerDefaultView?: string | null;
|
||||
/** The log columns to display for this source */
|
||||
logColumns?: UpdateSourceLogColumnInput[] | null;
|
||||
}
|
||||
|
@ -876,6 +884,10 @@ export namespace SourceConfigurationFields {
|
|||
|
||||
fields: Fields;
|
||||
|
||||
inventoryDefaultView: string;
|
||||
|
||||
metricsExplorerDefaultView: string;
|
||||
|
||||
logColumns: LogColumns[];
|
||||
};
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export const useFindSavedObject = <SavedObjectType extends SavedObjectAttributes
|
|||
const objects = await savedObjectsClient.find<SavedObjectType>({
|
||||
type,
|
||||
});
|
||||
return objects.savedObjects.filter((o) => o.attributes.name === name).length > 0;
|
||||
return objects.savedObjects.find((o) => o.attributes.name === name);
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
46
x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx
Normal file
46
x-pack/plugins/infra/public/hooks/use_get_saved_object.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { useState, useCallback } from 'react';
|
||||
import { SavedObjectAttributes, SimpleSavedObject } from 'src/core/public';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const useGetSavedObject = <SavedObjectType extends SavedObjectAttributes>(type: string) => {
|
||||
const kibana = useKibana();
|
||||
const [data, setData] = useState<SimpleSavedObject<SavedObjectType> | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const getObject = useCallback(
|
||||
(id: string) => {
|
||||
setLoading(true);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const savedObjectsClient = kibana.services.savedObjects?.client;
|
||||
if (!savedObjectsClient) {
|
||||
throw new Error('Saved objects client is unavailable');
|
||||
}
|
||||
const d = await savedObjectsClient.get<SavedObjectType>(type, id);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
setData(d);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
setError(e);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
},
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[type, kibana.services.savedObjects]
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
getObject,
|
||||
};
|
||||
};
|
|
@ -9,7 +9,7 @@ import { IHttpFetchError } from 'src/core/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { HttpHandler } from 'src/core/public';
|
||||
import { ToastInput } from 'src/core/public';
|
||||
import { useTrackedPromise } from '../utils/use_tracked_promise';
|
||||
import { useTrackedPromise, CanceledPromiseError } from '../utils/use_tracked_promise';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export function useHTTPRequest<Response>(
|
||||
|
@ -40,6 +40,9 @@ export function useHTTPRequest<Response>(
|
|||
onResolve: (resp) => setResponse(decode(resp)),
|
||||
onReject: (e: unknown) => {
|
||||
const err = e as IHttpFetchError;
|
||||
if (e && e instanceof CanceledPromiseError) {
|
||||
return;
|
||||
}
|
||||
setError(err);
|
||||
toast({
|
||||
toastLifeTimeMs: 3000,
|
||||
|
|
|
@ -1,90 +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 { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFindSavedObject } from './use_find_saved_object';
|
||||
import { useCreateSavedObject } from './use_create_saved_object';
|
||||
import { useDeleteSavedObject } from './use_delete_saved_object';
|
||||
|
||||
export type SavedView<ViewState> = ViewState & {
|
||||
name: string;
|
||||
id: string;
|
||||
isDefault?: boolean;
|
||||
};
|
||||
|
||||
export type SavedViewSavedObject<ViewState = {}> = ViewState & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const useSavedView = <ViewState>(defaultViewState: ViewState, viewType: string) => {
|
||||
const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject<
|
||||
SavedViewSavedObject<ViewState>
|
||||
>(viewType);
|
||||
const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType);
|
||||
const { deleteObject, deletedId } = useDeleteSavedObject(viewType);
|
||||
const deleteView = useCallback((id: string) => deleteObject(id), [deleteObject]);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => setCreateError(errorOnCreate), [errorOnCreate]);
|
||||
|
||||
const saveView = useCallback(
|
||||
(d: { [p: string]: any }) => {
|
||||
const doSave = async () => {
|
||||
const exists = await hasView(d.name);
|
||||
if (exists) {
|
||||
setCreateError(
|
||||
i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', {
|
||||
defaultMessage: `A view with that name already exists.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
create(d);
|
||||
};
|
||||
setCreateError(null);
|
||||
doSave();
|
||||
},
|
||||
[create, hasView]
|
||||
);
|
||||
|
||||
const savedObjects = useMemo(() => (data ? data.savedObjects : []), [data]);
|
||||
const views = useMemo(() => {
|
||||
const items: Array<SavedView<ViewState>> = [
|
||||
{
|
||||
name: i18n.translate('xpack.infra.savedView.defaultViewName', {
|
||||
defaultMessage: 'Default',
|
||||
}),
|
||||
id: '0',
|
||||
isDefault: true,
|
||||
...defaultViewState,
|
||||
},
|
||||
];
|
||||
|
||||
savedObjects.forEach(
|
||||
(o) =>
|
||||
o.type === viewType &&
|
||||
items.push({
|
||||
...o.attributes,
|
||||
id: o.id,
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [defaultViewState, savedObjects, viewType]);
|
||||
|
||||
return {
|
||||
views,
|
||||
saveView,
|
||||
loading,
|
||||
deletedId,
|
||||
createdId,
|
||||
errorOnFind,
|
||||
errorOnCreate: createError,
|
||||
deleteView,
|
||||
find,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { useState, useCallback } from 'react';
|
||||
import {
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsCreateOptions,
|
||||
SimpleSavedObject,
|
||||
} from 'src/core/public';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export const useUpdateSavedObject = (type: string) => {
|
||||
const kibana = useKibana();
|
||||
const [data, setData] = useState<SimpleSavedObject<SavedObjectAttributes> | null>(null);
|
||||
const [updatedId, setUpdatedId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const update = useCallback(
|
||||
(id: string, attributes: SavedObjectAttributes, options?: SavedObjectsCreateOptions) => {
|
||||
setLoading(true);
|
||||
const save = async () => {
|
||||
try {
|
||||
const savedObjectsClient = kibana.services.savedObjects?.client;
|
||||
if (!savedObjectsClient) {
|
||||
throw new Error('Saved objects client is unavailable');
|
||||
}
|
||||
const d = await savedObjectsClient.update(type, id, attributes, options);
|
||||
setUpdatedId(d.id);
|
||||
setError(null);
|
||||
setData(d);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
setError(e);
|
||||
}
|
||||
};
|
||||
save();
|
||||
},
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[type, kibana.services.savedObjects]
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
update,
|
||||
updatedId,
|
||||
};
|
||||
};
|
|
@ -6,16 +6,20 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
|
||||
import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { IIndexPattern } from 'src/plugins/data/common';
|
||||
import { DocumentTitle } from '../../components/document_title';
|
||||
import { HelpCenterContent } from '../../components/help_center_content';
|
||||
import { RoutedTabs } from '../../components/navigation/routed_tabs';
|
||||
import { ColumnarPage } from '../../components/page';
|
||||
import { Header } from '../../components/header';
|
||||
import { MetricsExplorerOptionsContainer } from './metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import {
|
||||
MetricsExplorerOptionsContainer,
|
||||
DEFAULT_METRICS_EXPLORER_VIEW_STATE,
|
||||
} from './metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state';
|
||||
import { WithSource } from '../../containers/with_source';
|
||||
import { Source } from '../../containers/source';
|
||||
|
@ -31,6 +35,8 @@ import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters
|
|||
|
||||
import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown';
|
||||
import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||
import { SavedView } from '../../containers/saved_view/saved_view';
|
||||
import { SourceConfigurationFields } from '../../graphql/types';
|
||||
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
|
||||
|
||||
const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', {
|
||||
|
@ -138,10 +144,9 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
|||
<MetricsExplorerOptionsContainer.Provider>
|
||||
<WithMetricsExplorerOptionsUrlState />
|
||||
{configuration ? (
|
||||
<MetricsExplorerPage
|
||||
derivedIndexPattern={createDerivedIndexPattern('metrics')}
|
||||
source={configuration}
|
||||
{...props}
|
||||
<PageContent
|
||||
configuration={configuration}
|
||||
createDerivedIndexPattern={createDerivedIndexPattern}
|
||||
/>
|
||||
) : (
|
||||
<SourceLoadingPage />
|
||||
|
@ -162,3 +167,25 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
|||
</EuiErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const PageContent = (props: {
|
||||
configuration: SourceConfigurationFields.Fragment;
|
||||
createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern;
|
||||
}) => {
|
||||
const { createDerivedIndexPattern, configuration } = props;
|
||||
const { options } = useContext(MetricsExplorerOptionsContainer.Context);
|
||||
|
||||
return (
|
||||
<SavedView.Provider
|
||||
shouldLoadDefault={options.source === 'default'}
|
||||
viewType={'metrics-explorer-view'}
|
||||
defaultViewState={DEFAULT_METRICS_EXPLORER_VIEW_STATE}
|
||||
>
|
||||
<MetricsExplorerPage
|
||||
derivedIndexPattern={createDerivedIndexPattern('metrics')}
|
||||
source={configuration}
|
||||
{...props}
|
||||
/>
|
||||
</SavedView.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
@ -20,14 +20,17 @@ import { InfraFormatterType } from '../../../../lib/lib';
|
|||
import { euiStyled } from '../../../../../../observability/public';
|
||||
import { Toolbar } from './toolbars/toolbar';
|
||||
import { ViewSwitcher } from './waffle/view_switcher';
|
||||
import { SavedViews } from './saved_views';
|
||||
import { IntervalLabel } from './waffle/interval_label';
|
||||
import { Legend } from './waffle/legend';
|
||||
import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
|
||||
import { createLegend } from '../lib/create_legend';
|
||||
import { useSavedViewContext } from '../../../../containers/saved_view/saved_view';
|
||||
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
|
||||
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
|
||||
|
||||
export const Layout = () => {
|
||||
const { sourceId, source } = useSourceContext();
|
||||
const { currentView, shouldLoadDefault } = useSavedViewContext();
|
||||
const {
|
||||
metric,
|
||||
groupBy,
|
||||
|
@ -78,6 +81,20 @@ export const Layout = () => {
|
|||
const bounds = autoBounds ? dataBounds : boundsOverride;
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
|
||||
const { viewState, onViewChange } = useWaffleViewState();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView) {
|
||||
onViewChange(currentView);
|
||||
}
|
||||
}, [currentView, onViewChange]);
|
||||
|
||||
useEffect(() => {
|
||||
// load snapshot data after default view loaded, unless we're not loading a view
|
||||
if (currentView != null || !shouldLoadDefault) {
|
||||
reload();
|
||||
}
|
||||
}, [reload, currentView, shouldLoadDefault]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -107,7 +124,7 @@ export const Layout = () => {
|
|||
<BottomActionContainer>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SavedViews />
|
||||
<SavedViewsToolbarControls viewState={viewState} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ position: 'relative', minWidth: 400 }}>
|
||||
<Legend
|
||||
|
|
|
@ -5,17 +5,9 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
|
||||
import { inventoryViewSavedObjectName } from '../../../../../common/saved_objects/inventory_view';
|
||||
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
|
||||
|
||||
export const SavedViews = () => {
|
||||
const { viewState, defaultViewState, onViewChange } = useWaffleViewState();
|
||||
return (
|
||||
<SavedViewsToolbarControls
|
||||
defaultViewState={defaultViewState}
|
||||
viewState={viewState}
|
||||
onViewChange={onViewChange}
|
||||
viewType={inventoryViewSavedObjectName}
|
||||
/>
|
||||
);
|
||||
const { viewState } = useWaffleViewState();
|
||||
return <SavedViewsToolbarControls viewState={viewState} />;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
@ -63,12 +62,6 @@ export function useSnapshot(
|
|||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await makeRequest();
|
||||
})();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error: (error && error.message) || null,
|
||||
loading,
|
||||
|
|
|
@ -39,6 +39,7 @@ export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = {
|
|||
steps: 10,
|
||||
reverseColors: false,
|
||||
},
|
||||
source: 'default',
|
||||
sort: { by: 'name', direction: 'desc' },
|
||||
};
|
||||
|
||||
|
@ -161,36 +162,44 @@ export const WaffleSortOptionRT = rt.type({
|
|||
direction: rt.keyof({ asc: null, desc: null }),
|
||||
});
|
||||
|
||||
export const WaffleOptionsStateRT = rt.type({
|
||||
metric: SnapshotMetricInputRT,
|
||||
groupBy: SnapshotGroupByRT,
|
||||
nodeType: ItemTypeRT,
|
||||
view: rt.string,
|
||||
customOptions: rt.array(
|
||||
rt.type({
|
||||
text: rt.string,
|
||||
field: rt.string,
|
||||
})
|
||||
),
|
||||
boundsOverride: rt.type({
|
||||
min: rt.number,
|
||||
max: rt.number,
|
||||
export const WaffleOptionsStateRT = rt.intersection([
|
||||
rt.type({
|
||||
metric: SnapshotMetricInputRT,
|
||||
groupBy: SnapshotGroupByRT,
|
||||
nodeType: ItemTypeRT,
|
||||
view: rt.string,
|
||||
customOptions: rt.array(
|
||||
rt.type({
|
||||
text: rt.string,
|
||||
field: rt.string,
|
||||
})
|
||||
),
|
||||
boundsOverride: rt.type({
|
||||
min: rt.number,
|
||||
max: rt.number,
|
||||
}),
|
||||
autoBounds: rt.boolean,
|
||||
accountId: rt.string,
|
||||
region: rt.string,
|
||||
customMetrics: rt.array(SnapshotCustomMetricInputRT),
|
||||
legend: WaffleLegendOptionsRT,
|
||||
sort: WaffleSortOptionRT,
|
||||
}),
|
||||
autoBounds: rt.boolean,
|
||||
accountId: rt.string,
|
||||
region: rt.string,
|
||||
customMetrics: rt.array(SnapshotCustomMetricInputRT),
|
||||
legend: WaffleLegendOptionsRT,
|
||||
sort: WaffleSortOptionRT,
|
||||
});
|
||||
rt.partial({ source: rt.string }),
|
||||
]);
|
||||
|
||||
export type WaffleSortOption = rt.TypeOf<typeof WaffleSortOptionRT>;
|
||||
export type WaffleOptionsState = rt.TypeOf<typeof WaffleOptionsStateRT>;
|
||||
const encodeUrlState = (state: WaffleOptionsState) => {
|
||||
return WaffleOptionsStateRT.encode(state);
|
||||
};
|
||||
const decodeUrlState = (value: unknown) =>
|
||||
pipe(WaffleOptionsStateRT.decode(value), fold(constant(undefined), identity));
|
||||
const decodeUrlState = (value: unknown) => {
|
||||
const state = pipe(WaffleOptionsStateRT.decode(value), fold(constant(undefined), identity));
|
||||
if (state) {
|
||||
state.source = 'url';
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const WaffleOptions = createContainer(useWaffleOptions);
|
||||
export const [WaffleOptionsProvider, useWaffleOptionsContext] = WaffleOptions;
|
||||
|
|
|
@ -16,6 +16,13 @@ import {
|
|||
WaffleFiltersState,
|
||||
} from './use_waffle_filters';
|
||||
|
||||
export const DEFAULT_WAFFLE_VIEW_STATE: WaffleViewState = {
|
||||
...DEFAULT_WAFFLE_OPTIONS_STATE,
|
||||
filterQuery: DEFAULT_WAFFLE_FILTERS_STATE,
|
||||
time: DEFAULT_WAFFLE_TIME_STATE.currentTime,
|
||||
autoReload: DEFAULT_WAFFLE_TIME_STATE.isAutoReloading,
|
||||
};
|
||||
|
||||
export const useWaffleViewState = () => {
|
||||
const {
|
||||
metric,
|
||||
|
@ -53,13 +60,6 @@ export const useWaffleViewState = () => {
|
|||
legend,
|
||||
};
|
||||
|
||||
const defaultViewState: WaffleViewState = {
|
||||
...DEFAULT_WAFFLE_OPTIONS_STATE,
|
||||
filterQuery: DEFAULT_WAFFLE_FILTERS_STATE,
|
||||
time: DEFAULT_WAFFLE_TIME_STATE.currentTime,
|
||||
autoReload: DEFAULT_WAFFLE_TIME_STATE.isAutoReloading,
|
||||
};
|
||||
|
||||
const onViewChange = useCallback(
|
||||
(newState: WaffleViewState) => {
|
||||
setWaffleOptionsState({
|
||||
|
@ -89,7 +89,7 @@ export const useWaffleViewState = () => {
|
|||
|
||||
return {
|
||||
viewState,
|
||||
defaultViewState,
|
||||
defaultViewState: DEFAULT_WAFFLE_VIEW_STATE,
|
||||
onViewChange,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,6 +22,9 @@ import { useTrackPageview } from '../../../../../observability/public';
|
|||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { Layout } from './components/layout';
|
||||
import { useLinkProps } from '../../../hooks/use_link_props';
|
||||
import { SavedView } from '../../../containers/saved_view/saved_view';
|
||||
import { DEFAULT_WAFFLE_VIEW_STATE } from './hooks/use_waffle_view_state';
|
||||
import { useWaffleOptionsContext } from './hooks/use_waffle_options';
|
||||
|
||||
export const SnapshotPage = () => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
|
@ -30,10 +33,12 @@ export const SnapshotPage = () => {
|
|||
isLoading,
|
||||
loadSourceFailureMessage,
|
||||
loadSource,
|
||||
source,
|
||||
metricIndicesExist,
|
||||
} = useContext(Source.Context);
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'inventory' });
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 });
|
||||
const { source: optionsSource } = useWaffleOptionsContext();
|
||||
|
||||
const tutorialLinkProps = useLinkProps({
|
||||
app: 'home',
|
||||
|
@ -53,12 +58,18 @@ export const SnapshotPage = () => {
|
|||
})
|
||||
}
|
||||
/>
|
||||
{isLoading ? (
|
||||
{isLoading && !source ? (
|
||||
<SourceLoadingPage />
|
||||
) : metricIndicesExist ? (
|
||||
<>
|
||||
<FilterBar />
|
||||
<Layout />
|
||||
<SavedView.Provider
|
||||
shouldLoadDefault={optionsSource === 'default'}
|
||||
viewType={'inventory-view'}
|
||||
defaultViewState={DEFAULT_WAFFLE_VIEW_STATE}
|
||||
>
|
||||
<Layout />
|
||||
</SavedView.Provider>
|
||||
</>
|
||||
) : hasFailedLoadingSource ? (
|
||||
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
|
||||
|
|
|
@ -23,8 +23,6 @@ import { MetricsExplorerGroupBy } from './group_by';
|
|||
import { MetricsExplorerAggregationPicker } from './aggregation';
|
||||
import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options';
|
||||
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
|
||||
import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state';
|
||||
import { metricsExplorerViewSavedObjectName } from '../../../../../common/saved_objects/metrics_explorer_view';
|
||||
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
|
||||
import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges';
|
||||
import { ToolbarPanel } from '../../../../components/toolbar_panel';
|
||||
|
@ -34,7 +32,6 @@ interface Props {
|
|||
timeRange: MetricsExplorerTimeOptions;
|
||||
options: MetricsExplorerOptions;
|
||||
chartOptions: MetricsExplorerChartOptions;
|
||||
defaultViewState: MetricExplorerViewState;
|
||||
onRefresh: () => void;
|
||||
onTimeChange: (start: string, end: string) => void;
|
||||
onGroupByChange: (groupBy: string | null | string[]) => void;
|
||||
|
@ -42,7 +39,6 @@ interface Props {
|
|||
onMetricsChange: (metrics: MetricsExplorerMetric[]) => void;
|
||||
onAggregationChange: (aggregation: MetricsExplorerAggregation) => void;
|
||||
onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void;
|
||||
onViewStateChange: (vs: MetricExplorerViewState) => void;
|
||||
}
|
||||
|
||||
export const MetricsExplorerToolbar = ({
|
||||
|
@ -57,8 +53,6 @@ export const MetricsExplorerToolbar = ({
|
|||
onAggregationChange,
|
||||
chartOptions,
|
||||
onChartOptionsChange,
|
||||
defaultViewState,
|
||||
onViewStateChange,
|
||||
}: Props) => {
|
||||
const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0;
|
||||
const [timepickerQuickRanges] = useKibanaUiSetting(UI_SETTINGS.TIMEPICKER_QUICK_RANGES);
|
||||
|
@ -123,14 +117,11 @@ export const MetricsExplorerToolbar = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<SavedViewsToolbarControls
|
||||
defaultViewState={defaultViewState}
|
||||
viewState={{
|
||||
options,
|
||||
chartOptions,
|
||||
currentTimerange: timeRange,
|
||||
}}
|
||||
viewType={metricsExplorerViewSavedObjectName}
|
||||
onViewChange={onViewStateChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ marginRight: 5 }}>
|
||||
|
|
|
@ -27,7 +27,8 @@ export interface MetricExplorerViewState {
|
|||
|
||||
export const useMetricsExplorerState = (
|
||||
source: SourceQuery.Query['source']['configuration'],
|
||||
derivedIndexPattern: IIndexPattern
|
||||
derivedIndexPattern: IIndexPattern,
|
||||
shouldLoadImmediately = true
|
||||
) => {
|
||||
const [refreshSignal, setRefreshSignal] = useState(0);
|
||||
const [afterKey, setAfterKey] = useState<string | null | Record<string, string | null>>(null);
|
||||
|
@ -40,13 +41,15 @@ export const useMetricsExplorerState = (
|
|||
setTimeRange,
|
||||
setOptions,
|
||||
} = useContext(MetricsExplorerOptionsContainer.Context);
|
||||
const { loading, error, data } = useMetricsExplorerData(
|
||||
const { loading, error, data, loadData } = useMetricsExplorerData(
|
||||
options,
|
||||
source,
|
||||
derivedIndexPattern,
|
||||
currentTimerange,
|
||||
afterKey,
|
||||
refreshSignal
|
||||
refreshSignal,
|
||||
undefined,
|
||||
shouldLoadImmediately
|
||||
);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
|
@ -144,7 +147,7 @@ export const useMetricsExplorerState = (
|
|||
handleLoadMore: setAfterKey,
|
||||
defaultViewState,
|
||||
onViewStateChange,
|
||||
|
||||
loadData,
|
||||
refreshSignal,
|
||||
afterKey,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import DateMath from '@elastic/datemath';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { HttpHandler } from 'src/core/public';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
import { SourceQuery } from '../../../../../common/graphql/types';
|
||||
|
@ -30,7 +30,8 @@ export function useMetricsExplorerData(
|
|||
timerange: MetricsExplorerTimeOptions,
|
||||
afterKey: string | null | Record<string, string | null>,
|
||||
signal: any,
|
||||
fetch?: HttpHandler
|
||||
fetch?: HttpHandler,
|
||||
shouldLoadImmediately = true
|
||||
) {
|
||||
const kibana = useKibana();
|
||||
const fetchFn = fetch ? fetch : kibana.services.http?.fetch;
|
||||
|
@ -40,7 +41,7 @@ export function useMetricsExplorerData(
|
|||
const [lastOptions, setLastOptions] = useState<MetricsExplorerOptions | null>(null);
|
||||
const [lastTimerange, setLastTimerange] = useState<MetricsExplorerTimeOptions | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = useCallback(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
@ -112,9 +113,15 @@ export function useMetricsExplorerData(
|
|||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
|
||||
// TODO: fix this dependency list while preserving the semantics
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options, source, timerange, signal, afterKey]);
|
||||
return { error, loading, data };
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldLoadImmediately) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [loadData, shouldLoadImmediately]);
|
||||
return { error, loading, data, loadData };
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export interface MetricsExplorerOptions {
|
|||
aggregation: MetricsExplorerAggregation;
|
||||
forceInterval?: boolean;
|
||||
dropLastBucket?: boolean;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface MetricsExplorerTimeOptions {
|
||||
|
@ -84,6 +85,13 @@ export const DEFAULT_METRICS: MetricsExplorerOptionsMetric[] = [
|
|||
export const DEFAULT_OPTIONS: MetricsExplorerOptions = {
|
||||
aggregation: 'avg',
|
||||
metrics: DEFAULT_METRICS,
|
||||
source: 'default',
|
||||
};
|
||||
|
||||
export const DEFAULT_METRICS_EXPLORER_VIEW_STATE = {
|
||||
options: DEFAULT_OPTIONS,
|
||||
chartOptions: DEFAULT_CHART_OPTIONS,
|
||||
currentTimerange: DEFAULT_TIMERANGE,
|
||||
};
|
||||
|
||||
function parseJsonOrDefault<Obj>(value: string | null, defaultValue: Obj): Obj {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { SourceQuery } from '../../../../common/graphql/types';
|
||||
|
@ -15,6 +15,7 @@ import { NoData } from '../../../components/empty_states';
|
|||
import { MetricsExplorerCharts } from './components/charts';
|
||||
import { MetricsExplorerToolbar } from './components/toolbar';
|
||||
import { useMetricsExplorerState } from './hooks/use_metric_explorer_state';
|
||||
import { useSavedViewContext } from '../../../containers/saved_view/saved_view';
|
||||
|
||||
interface MetricsExplorerPageProps {
|
||||
source: SourceQuery.Query['source']['configuration'];
|
||||
|
@ -37,13 +38,27 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl
|
|||
handleTimeChange,
|
||||
handleRefresh,
|
||||
handleLoadMore,
|
||||
defaultViewState,
|
||||
onViewStateChange,
|
||||
} = useMetricsExplorerState(source, derivedIndexPattern);
|
||||
loadData,
|
||||
} = useMetricsExplorerState(source, derivedIndexPattern, false);
|
||||
const { currentView, shouldLoadDefault } = useSavedViewContext();
|
||||
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' });
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView) {
|
||||
onViewStateChange(currentView);
|
||||
}
|
||||
}, [currentView, onViewStateChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView != null || !shouldLoadDefault) {
|
||||
// load metrics explorer data after default view loaded, unless we're not loading a view
|
||||
loadData();
|
||||
}
|
||||
}, [loadData, currentView, shouldLoadDefault]);
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<DocumentTitle
|
||||
|
@ -68,8 +83,6 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl
|
|||
onMetricsChange={handleMetricsChange}
|
||||
onAggregationChange={handleAggregationChange}
|
||||
onChartOptionsChange={setChartOptions}
|
||||
defaultViewState={defaultViewState}
|
||||
onViewStateChange={onViewStateChange}
|
||||
/>
|
||||
{error ? (
|
||||
<NoData
|
||||
|
|
|
@ -29,6 +29,8 @@ export const source = {
|
|||
logAlias: 'filebeat-*',
|
||||
metricAlias: 'metricbeat-*',
|
||||
logColumns: [],
|
||||
inventoryDefaultView: 'host',
|
||||
metricsExplorerDefaultView: 'host',
|
||||
fields: {
|
||||
host: 'host.name',
|
||||
container: 'container.id',
|
||||
|
|
|
@ -36,6 +36,10 @@ export const sourcesSchema = gql`
|
|||
metricAlias: String!
|
||||
"The alias to read log data from"
|
||||
logAlias: String!
|
||||
"Default view for inventory"
|
||||
inventoryDefaultView: String!
|
||||
"Default view for Metrics Explorer"
|
||||
metricsExplorerDefaultView: String!
|
||||
"The field mapping to use for this source"
|
||||
fields: InfraSourceFields!
|
||||
"The columns to use for log display"
|
||||
|
@ -128,6 +132,10 @@ export const sourcesSchema = gql`
|
|||
logAlias: String
|
||||
"The field mapping to use for this source"
|
||||
fields: UpdateSourceFieldsInput
|
||||
"Name of default inventory view"
|
||||
inventoryDefaultView: String
|
||||
"Default view for Metrics Explorer"
|
||||
metricsExplorerDefaultView: String
|
||||
"The log columns to display for this source"
|
||||
logColumns: [UpdateSourceLogColumnInput!]
|
||||
}
|
||||
|
|
|
@ -357,6 +357,10 @@ export interface UpdateSourceInput {
|
|||
logAlias?: string | null;
|
||||
/** The field mapping to use for this source */
|
||||
fields?: UpdateSourceFieldsInput | null;
|
||||
/** Name of default inventory view */
|
||||
inventoryDefaultView?: string | null;
|
||||
/** Default view for Metrics Explorer */
|
||||
metricsExplorerDefaultView?: string | null;
|
||||
/** The log columns to display for this source */
|
||||
logColumns?: UpdateSourceLogColumnInput[] | null;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = {
|
|||
tiebreaker: '_doc',
|
||||
timestamp: '@timestamp',
|
||||
},
|
||||
inventoryDefaultView: '0',
|
||||
metricsExplorerDefaultView: '0',
|
||||
logColumns: [
|
||||
{
|
||||
timestampColumn: {
|
||||
|
|
|
@ -30,6 +30,12 @@ export const infraSourceConfigurationSavedObjectType: SavedObjectsType = {
|
|||
logAlias: {
|
||||
type: 'keyword',
|
||||
},
|
||||
inventoryDefaultView: {
|
||||
type: 'keyword',
|
||||
},
|
||||
metricsExplorerDefaultView: {
|
||||
type: 'keyword',
|
||||
},
|
||||
fields: {
|
||||
properties: {
|
||||
container: {
|
||||
|
|
|
@ -7863,7 +7863,6 @@
|
|||
"xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。",
|
||||
"xpack.infra.registerFeatures.logsTitle": "ログ",
|
||||
"xpack.infra.sampleDataLinkLabel": "ログ",
|
||||
"xpack.infra.savedView.defaultViewName": "デフォルト",
|
||||
"xpack.infra.savedView.errorOnCreate.duplicateViewName": "その名前のビューは既に存在します",
|
||||
"xpack.infra.savedView.errorOnCreate.title": "ビューの保存中にエラーが発生しました。",
|
||||
"xpack.infra.savedView.findError.title": "ビューの読み込み中にエラーが発生しました。",
|
||||
|
@ -7997,9 +7996,7 @@
|
|||
"xpack.infra.waffle.savedViews.cancelButton": "キャンセル",
|
||||
"xpack.infra.waffle.savedViews.includeTimeFilterLabel": "ビューに時刻を保存",
|
||||
"xpack.infra.waffle.savedViews.includeTimeHelpText": "ビューが読み込まれるごとに現在選択された時刻の時間フィルターが変更されます。",
|
||||
"xpack.infra.waffle.savedViews.loadViewsLabel": "読み込み",
|
||||
"xpack.infra.waffle.savedViews.saveButton": "保存",
|
||||
"xpack.infra.waffle.savedViews.saveViewLabel": "保存",
|
||||
"xpack.infra.waffle.savedViews.viewNamePlaceholder": "名前",
|
||||
"xpack.infra.waffle.selectTwoGroupingsTitle": "最大 2 つのグループ分けを選択してください",
|
||||
"xpack.infra.waffle.unableToSelectGroupErrorMessage": "{nodeType} のオプションでグループを選択できません",
|
||||
|
|
|
@ -7868,7 +7868,6 @@
|
|||
"xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。",
|
||||
"xpack.infra.registerFeatures.logsTitle": "鏃ュ織",
|
||||
"xpack.infra.sampleDataLinkLabel": "日志",
|
||||
"xpack.infra.savedView.defaultViewName": "默认值",
|
||||
"xpack.infra.savedView.errorOnCreate.duplicateViewName": "具有该名称的视图已存在。",
|
||||
"xpack.infra.savedView.errorOnCreate.title": "保存视图时出错。",
|
||||
"xpack.infra.savedView.findError.title": "加载视图时出错。",
|
||||
|
@ -8002,9 +8001,7 @@
|
|||
"xpack.infra.waffle.savedViews.cancelButton": "取消",
|
||||
"xpack.infra.waffle.savedViews.includeTimeFilterLabel": "将时间与视图一起存储",
|
||||
"xpack.infra.waffle.savedViews.includeTimeHelpText": "每次加载此仪表板时,这都会将时间筛选更改为当前选定的时间",
|
||||
"xpack.infra.waffle.savedViews.loadViewsLabel": "负载",
|
||||
"xpack.infra.waffle.savedViews.saveButton": "保存",
|
||||
"xpack.infra.waffle.savedViews.saveViewLabel": "保存",
|
||||
"xpack.infra.waffle.savedViews.viewNamePlaceholder": "名称",
|
||||
"xpack.infra.waffle.selectTwoGroupingsTitle": "选择最多两个分组",
|
||||
"xpack.infra.waffle.unableToSelectGroupErrorMessage": "无法选择 {nodeType} 的分组依据选项",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue