Skip to content

Commit

Permalink
Add custom saved queries and fix duplicate requests (#7090)
Browse files Browse the repository at this point in the history
* Add custom saved queries

* Add changelog

* Add setTimeFilter function to save and update saved queries

* Remove absolute date and add fingerprint to lastReloadRequestTime

* Add explanation to fingerprint state

* Add support for setting and updating refresh interval

* Update saveSavedQuery function to handle newSavedQuery

* Refactor saved query functions for better organization

* Refactor functions to accept options for first time use

* Add absolute date range for data grids

* Refactor saved query hooks for setQuery and setFilters

* Remove saved query clearing functionality

* Refactor useSavedQuery function for better readability

* Delete unused test and function for saved queries

* Add jest spy for setFilters in useSearchBar hook test

* Fix search-bar autorefresh feature

* Apply autoRefreshFingerprint in dashboards

* Add fingerprint to Vulnerabilities module

* Refactor useSearchBar test for readability

* Fix unit test

* Prettier

* Hide save query button in flyout search bar

* Fix absolute date string format

* By default show the save query button in the saved queries popover

---------

Co-authored-by: Guido Modarelli <[email protected]>
  • Loading branch information
asteriscos and guidomodarelli authored Oct 15, 2024
1 parent c3559de commit 79837fe
Show file tree
Hide file tree
Showing 48 changed files with 809 additions and 294 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ All notable changes to the Wazuh app project will be documented in this file.
- Fixed style when unnpinned an agent in endpoint summary section [#7015](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7015)
- Fixed overflow style on a long value filter [#7021](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7021)
- Fixed buttons enabled for a readonly user in `Endpoint groups` section [#7056](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7056)
- Fixed the automatic page refresh in dashboards and prevent duplicate requests [#7090](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7090)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions plugins/main/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,6 @@ export const SEARCH_BAR_DEBOUNCE_UPDATE_TIME = 400;

// ID used to refer the createOsdUrlStateStorage state
export const OSD_URL_STATE_STORAGE_ID = 'state:storeInSessionStorage';

export const APP_STATE_URL_KEY = '_a';
export const GLOBAL_STATE_URL_KEY = '_g';
Original file line number Diff line number Diff line change
Expand Up @@ -236,5 +236,6 @@ export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => {
onChangeItemsPerPage: onChangeItemsPerPage,
onChangePage: onChangePage,
},
setPagination,
} as EuiDataGridProps;
};
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export function useDataSource<
const {
filters: initialFilters = [...defaultFilters],
fetchFilters: initialFetchFilters = [],
fixedFilters: initialFixedFilters = [],
DataSource: DataSourceConstructor,
repository,
factory: injectedFactory,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SAVED_QUERY = 'savedQuery';
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { useState, useEffect } from 'react';
import {
DataPublicPluginStart,
RefreshInterval,
SavedQuery,
TimeRange,
Query,
Filter,
} from '../../../../../../../src/plugins/data/public';
import NavigationService from '../../../../react-services/navigation-service';
import { OSD_URL_STATE_STORAGE_ID } from '../../../../../common/constants';
import { getDataPlugin, getUiSettings } from '../../../../kibana-services';
import { createOsdUrlStateStorage } from '../../../../../../../src/plugins/opensearch_dashboards_utils/public';
import OsdUrlStateStorage from '../../../../react-services/state-storage';

interface Options {
firstTime?: boolean;
}

interface UseSavedQueriesProps {
queryService: DataPublicPluginStart['query'];
setTimeFilter: (timeFilter: TimeRange) => void;
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
setQuery: (query?: Query) => void;
setFilters: (filters: Filter[]) => void;
}

interface UseSavedQueriesReturn {
savedQuery?: SavedQuery;
setSavedQuery: (savedQuery: SavedQuery) => void;
clearSavedQuery: () => void;
}

export const useSavedQuery = (
props: UseSavedQueriesProps,
): UseSavedQueriesReturn => {
// Handle saved queries
const [savedQuery, setSavedQuery] = useState<SavedQuery | undefined>();
const data = getDataPlugin();
const config = getUiSettings();
const history = NavigationService.getInstance().getHistory();
const osdUrlStateStorage = createOsdUrlStateStorage({
useHash: config.get(OSD_URL_STATE_STORAGE_ID),
history: history,
});

const getAppFilters = (
newSavedQuery?: SavedQuery,
{ firstTime }: Options = { firstTime: false },
) => {
const { filterManager } = props.queryService;
// When the page reloads, savedQuery starts as undefined, so retrieve the time and refreshInterval from the URL.
return firstTime
? filterManager.getAppFilters()
: newSavedQuery?.attributes.filters ?? [];
};

const getQuery = (
newSavedQuery?: SavedQuery,
{ firstTime }: Options = { firstTime: false },
) => {
const { queryString } = props.queryService;
// When the page reloads, savedQuery starts as undefined, so retrieve the time and refreshInterval from the URL.
return firstTime
? queryString.getQuery()
: newSavedQuery?.attributes.query ?? { query: '', language: 'kuery' };
};

const getTimeFilter = (
newSavedQuery?: SavedQuery,
{ firstTime }: Options = { firstTime: false },
) => {
const { timefilter } = props.queryService;
// When the page reloads, savedQuery starts as undefined, so retrieve the time and refreshInterval from the URL.
return firstTime
? timefilter.timefilter.getTime()
: newSavedQuery?.attributes.timefilter;
};

const getRefreshInterval = (
newSavedQuery?: SavedQuery,
{ firstTime }: Options = { firstTime: false },
) => {
const { timefilter } = props.queryService;
// When the page reloads, savedQuery starts as undefined, so retrieve the time and refreshInterval from the URL.
return firstTime
? timefilter.timefilter.getRefreshInterval()
: newSavedQuery?.attributes.timefilter?.refreshInterval;
};

const setTimeFilter = (timeFilter: TimeRange) => {
props.setTimeFilter(timeFilter);
props.queryService.timefilter.timefilter.setTime(timeFilter);
};

const saveSavedQuery = async (
newSavedQuery?: SavedQuery,
{ firstTime }: Options = { firstTime: false },
) => {
setSavedQuery(newSavedQuery);
const filters = getAppFilters(newSavedQuery, { firstTime });
const query = getQuery(newSavedQuery, { firstTime });
await OsdUrlStateStorage(data, osdUrlStateStorage).replaceUrlAppState({
savedQuery: newSavedQuery?.id,
});
props.setFilters(filters);
props.setQuery(query);
if (newSavedQuery?.attributes.timefilter) {
setTimeFilter(getTimeFilter(newSavedQuery, { firstTime }));
props.setRefreshInterval(
getRefreshInterval(newSavedQuery, { firstTime }),
);
}
};

const updateSavedQuery = async (
savedQuery: SavedQuery,
{ firstTime }: { firstTime?: boolean } = { firstTime: false },
) => {
saveSavedQuery(savedQuery, { firstTime });
};

const clearSavedQuery = () => {
// remove saved query from url
saveSavedQuery(undefined);
};

// Effect is used to convert a saved query id into an object
useEffect(() => {
const fetchSavedQuery = async () => {
try {
const savedQueryId = OsdUrlStateStorage(
data,
osdUrlStateStorage,
).getAppStateFromUrl().savedQuery as string;
if (!savedQueryId) return;
// fetch saved query
const savedQuery = await props.queryService.savedQueries.getSavedQuery(
savedQueryId,
);
updateSavedQuery(savedQuery, { firstTime: true });
} catch (error) {
clearSavedQuery();
}
};

fetchSavedQuery();
}, [props.queryService, props.queryService.savedQueries]);

return {
savedQuery,
setSavedQuery: updateSavedQuery,
clearSavedQuery,
};
};
17 changes: 16 additions & 1 deletion plugins/main/public/components/common/hooks/use-time-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,30 @@ export function useTimeFilter() {
const [timeFilter, setTimeFilter] = useState(
globalStateFromUrl?.time ?? timefilter.getTime(),
);
const [refreshInterval, setRefreshInterval] = useState(
globalStateFromUrl?.refreshInterval ?? timefilter.getRefreshInterval(),
);
const [timeHistory, setTimeHistory] = useState(timefilter._history);
useEffect(() => {
const subscription = timefilter.getTimeUpdate$().subscribe(() => {
setTimeFilter(timefilter.getTime());
setTimeHistory(timefilter._history);
});
const subscriptionRefreshInterval = timefilter
.getRefreshIntervalUpdate$()
.subscribe(() => {
setRefreshInterval(timefilter.getRefreshInterval());
});
return () => {
subscription.unsubscribe();
subscriptionRefreshInterval.unsubscribe();
};
}, []);
return { timeFilter, setTimeFilter: timefilter.setTime, timeHistory };
return {
timeFilter,
setTimeFilter: timefilter.setTime,
refreshInterval,
setRefreshInterval: timefilter.setRefreshInterval,
timeHistory,
};
}
17 changes: 9 additions & 8 deletions plugins/main/public/components/common/permissions/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ import {
EuiButtonEmpty,
EuiButtonIcon,
EuiLink,
EuiButtonProps,
} from '@elastic/eui';

import { IWzElementPermissionsProps, WzElementPermissions } from './element';

interface IWzButtonPermissionsProps
extends Omit<
IWzElementPermissionsProps,
'children' | 'additionalPropsFunction'
> {
buttonType?: 'default' | 'empty' | 'icon' | 'link' | 'switch';
rest: any;
}
type IWzButtonPermissionsProps = Omit<
IWzElementPermissionsProps,
'children' | 'additionalPropsFunction'
> &
React.ButtonHTMLAttributes<HTMLButtonElement> &
EuiButtonProps & {
buttonType?: 'default' | 'empty' | 'icon' | 'link' | 'switch';
};

export const WzButtonPermissions = ({
buttonType = 'default',
Expand Down
10 changes: 9 additions & 1 deletion plugins/main/public/components/common/search-bar/search-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ export interface WzSearchBarProps extends SearchBarProps {
preQueryBar?: React.ReactElement;
postFilters?: React.ReactElement;
hideFixedFilters?: boolean;
showSaveQueryButton?: boolean;
showSaveQuery?: boolean;
}

export const WzSearchBar = ({
fixedFilters = [],
showSaveQueryButton = true,
showSaveQuery = true,
preQueryBar,
hideFixedFilters,
postFilters,
Expand Down Expand Up @@ -48,7 +52,11 @@ export const WzSearchBar = ({
>
{preQueryBar ? <EuiFlexItem>{preQueryBar}</EuiFlexItem> : null}
<EuiFlexItem grow={!preQueryBar}>
<SearchBar {...restProps} />
<SearchBar
{...restProps}
showFilterBar={showSaveQueryButton}
showSaveQuery={showSaveQuery}
/>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
Expand Down
Loading

0 comments on commit 79837fe

Please sign in to comment.