Skip to content

Commit

Permalink
[Infrastructure UI] Adopt new saved views API (#155827)
Browse files Browse the repository at this point in the history
## 📓 Summary

Part of #152617
Closes #106650
Closes #154725

This PR replace the existing client React logic for handling saved views
on the `infra` plugin with a new state management implementation that
interacts with the newly created API.
It brings the following changes:
- Implement `useInventoryViews` and `useMetricsExplorerViews` custom
hooks.
- Adopt `@tanstack/react-query` for the above hooks implementation as it
was already used across the plugin and simplifies the server state
management. Extract the provider for the react query cache.
- Update server services to fix an issue while updating views, which was
preventing the unset of properties from the view.
- Update Saved Views components to integrate the new hooks.
- The `Load views` option has been removed accordingly to the decision
made with the UX team, since it wasn't adding any value that wasn't
already present in the `Manage views` option.

Even if we are duplicating similar logic to handle the Inventory and
Metrics Explorer views, we decided to keep them separated to easily
control their behaviour and avoid coupled logic that can be painful to
split in future.

## 🐞 Bug fixes

This implementation also fixed some existing bugs in production:
- As reported in [this
comment](#155174 (review)),
when the current view is deleted, the selector doesn't fallback on
another view and keeps showing the same view title.
It has been fixed and the selected view fallbacks to the default view.
- When refreshing the page after a view was selected, the view was not
correctly recovered and shown. The implemented changes fix this
behaviour.
- The "include time" option for creating/updating a saved view was not
working and never removed the time property if disabled.
- Minor visual adjustments such as action button type and alignment.

## 👨‍💻 Review hints

The best way to verify all the interactions and loadings work correctly
as a user expects, running the branch locally with an oblt cluster is
recommended.
In both the inventory and metrics explorer pages, the user should be
able to:
- Access and manage the saved views, select and load a view, delete a
view, and set a view as default.
- Save a new view.
- Update the currently used view, except for the static **Default
view**.
- Show an error when trying to create/update a view with a name already
held by another view.
- Restore the view with the following priority order
  - Use from the URL the stored view id to restore the view
- Use the default view id stored in the source configuration as a user
preference
  - Use the static **Default view**

## 👣 Following steps

- [ ] #155117

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: jennypavlova <jennypavlova94@gmail.com>
  • Loading branch information
4 people authored May 2, 2023
1 parent bf0920d commit 74c9814
Show file tree
Hide file tree
Showing 36 changed files with 1,306 additions and 834 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useState } from 'react';
import React, { useMemo } from 'react';
import useToggle from 'react-use/lib/useToggle';

import {
Expand All @@ -22,107 +22,85 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { SavedView } from '../../containers/saved_view/saved_view';
import { EuiBasicTableColumn } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { MetricsExplorerView } from '../../../common/metrics_explorer_views';
import type { InventoryView } from '../../../common/inventory_views';
import { UseInventoryViewsResult } from '../../hooks/use_inventory_views';
import { UseMetricsExplorerViewsResult } from '../../hooks/use_metrics_explorer_views';

interface Props<ViewState> {
views: Array<SavedView<ViewState>>;
type View = InventoryView | MetricsExplorerView;
type UseViewResult = UseInventoryViewsResult | UseMetricsExplorerViewsResult;

export interface ManageViewsFlyoutProps {
views: UseViewResult['views'];
loading: boolean;
sourceIsLoading: boolean;
onClose(): void;
onMakeDefaultView(id: string): void;
setView(viewState: ViewState): void;
onDeleteView(id: string): void;
onMakeDefaultView: UseViewResult['setDefaultViewById'];
onSwitchView: UseViewResult['switchViewById'];
onDeleteView: UseViewResult['deleteViewById'];
}

interface DeleteConfimationProps {
isDisabled?: boolean;
onConfirm(): void;
}

const DeleteConfimation = ({ isDisabled, onConfirm }: DeleteConfimationProps) => {
const [isConfirmVisible, toggleVisibility] = useToggle(false);

return isConfirmVisible ? (
<EuiFlexGroup>
<EuiButtonEmpty onClick={toggleVisibility} data-test-subj="hideConfirm">
<FormattedMessage defaultMessage="cancel" id="xpack.infra.waffle.savedViews.cancel" />
</EuiButtonEmpty>
<EuiButton
disabled={isDisabled}
fill={true}
iconType="trash"
color="danger"
onClick={onConfirm}
data-test-subj="showConfirm"
>
<FormattedMessage
defaultMessage="Delete view?"
id="xpack.infra.openView.actionNames.deleteConfirmation"
/>
</EuiButton>
</EuiFlexGroup>
) : (
<EuiButtonEmpty
data-test-subj="infraDeleteConfimationButton"
iconType="trash"
color="danger"
onClick={toggleVisibility}
/>
);
const searchConfig = {
box: { incremental: true },
};

export function SavedViewManageViewsFlyout<ViewState>({
export function ManageViewsFlyout({
onClose,
views,
setView,
views = [],
onSwitchView,
onMakeDefaultView,
onDeleteView,
loading,
sourceIsLoading,
}: Props<ViewState>) {
const [inProgressView, setInProgressView] = useState<string | null>(null);
}: ManageViewsFlyoutProps) {
// Add name as top level property to allow in memory search
const namedViews = useMemo(() => views.map(addOwnName), [views]);

const renderName = (name: string, item: SavedView<ViewState>) => (
const renderName = (name: string, item: View) => (
<EuiButtonEmpty
key={item.id}
data-test-subj="infraRenderNameButton"
onClick={() => {
setView(item);
onSwitchView(item.id);
onClose();
}}
>
{name}
</EuiButtonEmpty>
);

const renderDeleteAction = (item: SavedView<ViewState>) => {
const renderDeleteAction = (item: View) => {
return (
<DeleteConfimation
key={item.id}
isDisabled={item.isDefault}
isDisabled={item.attributes.isDefault}
onConfirm={() => {
onDeleteView(item.id);
}}
/>
);
};

const renderMakeDefaultAction = (item: SavedView<ViewState>) => {
const renderMakeDefaultAction = (item: View) => {
return (
<EuiButtonEmpty
<EuiButtonIcon
key={item.id}
data-test-subj="infraRenderMakeDefaultActionButton"
isLoading={inProgressView === item.id && sourceIsLoading}
iconType={item.isDefault ? 'starFilled' : 'starEmpty'}
iconType={item.attributes.isDefault ? 'starFilled' : 'starEmpty'}
size="s"
onClick={() => {
setInProgressView(item.id);
onMakeDefaultView(item.id);
}}
/>
);
};

const columns = [
const columns: Array<EuiBasicTableColumn<View>> = [
{
field: 'name',
name: i18n.translate('xpack.infra.openView.columnNames.name', { defaultMessage: 'Name' }),
Expand All @@ -139,7 +117,7 @@ export function SavedViewManageViewsFlyout<ViewState>({
render: renderMakeDefaultAction,
},
{
available: (item: SavedView<ViewState>) => item.id !== '0',
available: (item) => !item.attributes.isStatic,
render: renderDeleteAction,
},
],
Expand All @@ -161,10 +139,10 @@ export function SavedViewManageViewsFlyout<ViewState>({
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiInMemoryTable
items={views}
items={namedViews}
columns={columns}
loading={loading}
search={true}
search={searchConfig}
pagination={true}
sorting={true}
/>
Expand All @@ -178,3 +156,41 @@ export function SavedViewManageViewsFlyout<ViewState>({
</EuiPortal>
);
}

const DeleteConfimation = ({ isDisabled, onConfirm }: DeleteConfimationProps) => {
const [isConfirmVisible, toggleVisibility] = useToggle(false);

return isConfirmVisible ? (
<EuiFlexGroup>
<EuiButtonEmpty onClick={toggleVisibility} data-test-subj="hideConfirm">
<FormattedMessage defaultMessage="cancel" id="xpack.infra.waffle.savedViews.cancel" />
</EuiButtonEmpty>
<EuiButton
disabled={isDisabled}
fill={true}
iconType="trash"
color="danger"
onClick={onConfirm}
data-test-subj="showConfirm"
>
<FormattedMessage
defaultMessage="Delete view?"
id="xpack.infra.openView.actionNames.deleteConfirmation"
/>
</EuiButton>
</EuiFlexGroup>
) : (
<EuiButtonIcon
data-test-subj="infraDeleteConfimationButton"
iconType="trash"
color="danger"
size="s"
onClick={toggleVisibility}
/>
);
};

/**
* Helpers
*/
const addOwnName = (view: View) => ({ ...view, name: view.attributes.name });
Loading

0 comments on commit 74c9814

Please sign in to comment.