mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Fleet] ON Week - Port Integrations UI to @tanstack/query
(#146714)
## Summary
Migrates data fetching in `applications/integrations` to
`@tanstack/query` - https://tanstack.com/query/latest
This migration gives us some powerful tools around data fetching and
state management in React, and follows suit with several other Kibana
plugins that have migrated to `react-query` in the recent months.
I took some time to write up some dev docs around this change in
bf9527a2bf/x-pack/plugins/fleet/dev_docs/react_query.md
that should hopefully represent a good start in explaining the new data
fetching patterns in detail.
Note: I'm reviving this ON week PR from November after a few months, so
there might be non-RQ data fetching changes that have landed in `main`
since I initially filed these changes. If you find anything please let
me know and I'll happily migrate that too.
I'll also be doing some more manual testing of critical integrations UX
flows to make sure there's no regressions here, though everything I can
think of has looked good so far 🙂
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d87733160c
commit
031f67bb49
33 changed files with 818 additions and 416 deletions
|
@ -60,7 +60,7 @@ pageLoadAssetSize:
|
|||
files: 22673
|
||||
filesManagement: 18683
|
||||
fileUpload: 25664
|
||||
fleet: 126917
|
||||
fleet: 142263
|
||||
globalSearch: 29696
|
||||
globalSearchBar: 50403
|
||||
globalSearchProviders: 25554
|
||||
|
|
238
x-pack/plugins/fleet/dev_docs/react_query.md
Normal file
238
x-pack/plugins/fleet/dev_docs/react_query.md
Normal file
|
@ -0,0 +1,238 @@
|
|||
# `@tanstack/query` Usage in Fleet + Integrations
|
||||
|
||||
This document seeks to outline the Fleet + Integrations apps' usage of [`@tanstack/query`](https://tanstack.com/query/latest) - formally known generally as `react-query`. When we talk about the React-specific adapter for `@tanstack/query`, we'll use the library name `react-query`. Since Kibana doesn't have Vue, Solid, or Svelte plugins, we don't need to be worried about the other client implementations. This is a library for asynchronous state management that's most commonly utilized for data fetching logic. `@tanstack/query` helps developers write consistent state management logic around asynchronous operations, while providing end users with a performant, "jank-free" experience.
|
||||
|
||||
## Helpful Links
|
||||
|
||||
- `react-query` [docs](https://tanstack.com/query/latest/docs/react/overview)
|
||||
- [Practical React Query](https://tanstack.com/query/latest/docs/react/overview) by maintainer [TkDodo](https://github.com/tkdodo)
|
||||
- This series is long but extremely helpful. A highly recommended read for anyone working with data fetching in Fleet/Integrations!
|
||||
- `@tanstack/query` source code on GitHub: https://github.com/TanStack/query
|
||||
|
||||
## How Fleet/Integrations uses custom data fetching hooks
|
||||
|
||||
Historically, Fleet/Integrations have used homegrown data fetching hooks in a common folder at [`public/hooks/use_request`](https://github.com/elastic/kibana/tree/main/x-pack/plugins/fleet/public/hooks/use_request). Each `.ts` file in this directory contains one or more data fetching hooks related to a particular resource or concept. For example, here's what some data fetching hooks for `packages` and `categories` might look like:
|
||||
|
||||
```ts
|
||||
// use_request/epm.ts
|
||||
|
||||
export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
||||
return useRequest<GetCategoriesResponse>({
|
||||
path: epmRouteService.getCategoriesPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
||||
return sendRequest<GetCategoriesResponse>({
|
||||
path: epmRouteService.getCategoriesPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => {
|
||||
return useRequest<GetPackagesResponse>({
|
||||
path: epmRouteService.getListPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendGetPackages = (query: GetPackagesRequest['query'] = {}) => {
|
||||
return sendRequest<GetPackagesResponse>({
|
||||
path: epmRouteService.getListPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>What are <code>useRequest</code> and <code>sendRequest</code>?</summary>
|
||||
|
||||
The `useRequest` and `sendRequest` methods are common across all of these data fetching hooks, and use Kibana's provide `useRequest` hook and `sendRequest` helper with some additional logic on top. e.g.
|
||||
|
||||
```ts
|
||||
// use_request/use_request.ts - excerpts for clarity
|
||||
|
||||
import {
|
||||
sendRequest as _sendRequest,
|
||||
useRequest as _useRequest,
|
||||
} from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
export const sendRequest = <D = any, E = RequestError>(
|
||||
config: SendRequestConfig
|
||||
): Promise<SendRequestResponse<D, E>> => {
|
||||
if (!httpClient) {
|
||||
throw new Error('sendRequest has no http client set');
|
||||
}
|
||||
return _sendRequest<D, E>(httpClient, config);
|
||||
};
|
||||
|
||||
export const useRequest = <D = any, E = RequestError>(config: UseRequestConfig) => {
|
||||
if (!httpClient) {
|
||||
throw new Error('sendRequest has no http client set');
|
||||
}
|
||||
return _useRequest<D, E>(httpClient, config);
|
||||
};
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Consuming these data fetching hooks might look something like this
|
||||
|
||||
```tsx
|
||||
// applications/integrations/sections/epm/screens/detail/settings/update_button.tsx
|
||||
const handleClickUpgradePolicies = useCallback(async () => {
|
||||
if (isUpgradingPackagePolicies) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdateModalVisible(false);
|
||||
setIsUpgradingPackagePolicies(true);
|
||||
|
||||
await installPackage({ name, version, title });
|
||||
|
||||
await sendUpgradePackagePolicy(
|
||||
// Only upgrade policies that don't have conflicts
|
||||
packagePolicyIds.filter(
|
||||
(id) => !dryRunData?.find((dryRunRecord) => dryRunRecord.diff?.[0].id === id)?.hasErrors
|
||||
)
|
||||
);
|
||||
|
||||
setIsUpgradingPackagePolicies(false);
|
||||
|
||||
notifications.toasts.addSuccess({
|
||||
title: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessTitle"
|
||||
defaultMessage="Updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessDescription"
|
||||
defaultMessage="Successfully updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
});
|
||||
|
||||
navigateToNewSettingsPage();
|
||||
}, [
|
||||
dryRunData,
|
||||
installPackage,
|
||||
isUpgradingPackagePolicies,
|
||||
name,
|
||||
navigateToNewSettingsPage,
|
||||
notifications.toasts,
|
||||
packagePolicyIds,
|
||||
setIsUpgradingPackagePolicies,
|
||||
title,
|
||||
version,
|
||||
theme$,
|
||||
]);
|
||||
```
|
||||
|
||||
In the "custom data fetching" hooks world, there are a few big problems:
|
||||
|
||||
1. Caching, cancellation, deduping/debouncing successive requests, and optimizations around re-renders are an afterthought
|
||||
2. Mutations in particular are extremely verbose, as we need to "wire up" all error/loading state, "post-mutation" operations, etc
|
||||
3. Revalidating queries from elsewhere in the component tree (e.g. update the agent policy table when a new agent policy is saved) is a tricky operation usually solved by intermittent polling or a `location.reload()`
|
||||
|
||||
## How `react-query` helps
|
||||
|
||||
`react-query` handles many of the "big problems" above out-of-the-box. By providing a basic key/value based cache for queries, consistent utilities around state transitions of async operations, and robust revalidation helpers, `react-query` makes working with the state around data fetching much more predictable and pleasant.
|
||||
|
||||
### How Fleet/Integrations uses `react-query`
|
||||
|
||||
There's a bit of setup involved to actually get `react-query` up and running. First and foremost, each Kibana application is wrapped in a `<QueryClientProvider>` that handles `react-query`'s internal query cache and various React context needs. e.g.
|
||||
|
||||
```tsx
|
||||
//...
|
||||
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={true} />
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<Router history={history}>
|
||||
<PackageInstallProvider notifications={startServices.notifications} theme$={theme$}>
|
||||
<FlyoutContextProvider>{children}</FlyoutContextProvider>
|
||||
</PackageInstallProvider>
|
||||
</Router>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
</QueryClientProvider>
|
||||
</EuiThemeProvider>
|
||||
```
|
||||
|
||||
We also set up `react-query`'s [dev tools](https://tanstack.com/query/v4/docs/react/devtools), which provide a useful developer console for debugging query and mutation state across the whole application.
|
||||
|
||||
Another step required to use `react-query` in Fleet/Integrations is the introduction of a specialized data fetching utility. `react-query` operations expect a slightly different structure than what Kibana's `useRequest` and `sendRequest` helpers. For this purpose, we introduce the `sendRequestForRq` helper, e.g.
|
||||
|
||||
```ts
|
||||
// Sends requests with better ergonomics for React Query, e.g. throw error rather
|
||||
// than resolving with an `error` property in the result. Also returns `data` directly
|
||||
// as opposed to { data } in a response object.
|
||||
export const sendRequestForRq = async <D = any, E = RequestError>(
|
||||
config: SendRequestConfig
|
||||
): Promise<D> => {
|
||||
if (!httpClient) {
|
||||
throw new Error('sendRequest has no http client set');
|
||||
}
|
||||
|
||||
const response = await _sendRequest<D, E>(httpClient, config);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
// Data can't be null so long as `_sendRequest` did not throw
|
||||
return response.data!;
|
||||
};
|
||||
```
|
||||
|
||||
So, with those pieces of setup in mind, adding a new query or mutation looks like this:
|
||||
|
||||
```ts
|
||||
export function useGetCategoriesQuery(query: GetCategoriesRequest['query'] = {}) {
|
||||
return useQuery<GetCategoriesResponse, RequestError>(['categories', query], () =>
|
||||
sendRequestForRq<GetCategoriesResponse>({
|
||||
path: epmRouteService.getCategoriesPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const useGetPackagesQuery = (query: GetPackagesRequest['query']) => {
|
||||
return useQuery<GetPackagesResponse, RequestError>(['get-packages', query.prerelease], () =>
|
||||
sendRequestForRq<GetPackagesResponse>({
|
||||
path: epmRouteService.getListPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdatePackageMutation = () => {
|
||||
return useMutation<UpdatePackageResponse, RequestError, UpdatePackageArgs>(
|
||||
({ pkgName, pkgVersion, body }: UpdatePackageArgs) =>
|
||||
sendRequestForRq<UpdatePackageResponse>({
|
||||
path: epmRouteService.getUpdatePath(pkgName, pkgVersion),
|
||||
method: 'put',
|
||||
body,
|
||||
})
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### `react-query` operation naming conventions
|
||||
|
||||
For `react-query` operations defined in `use_request/`, try to use a naming convention along the lines of `use{Action}{Resource}{Query/Mutation}` for your hooks. This helps with consistency and makes the intent of every data fetching operation clear.
|
|
@ -15,6 +15,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import type { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||
|
||||
|
@ -62,6 +64,8 @@ import { DebugPage } from './sections/debug';
|
|||
|
||||
const FEEDBACK_URL = 'https://ela.st/fleet-feedback';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const ErrorLayout: FunctionComponent<{ isAddIntegrationsPath: boolean }> = ({
|
||||
isAddIntegrationsPath,
|
||||
children,
|
||||
|
@ -257,18 +261,21 @@ export const FleetAppContext: React.FC<{
|
|||
<KibanaVersionContext.Provider value={kibanaVersion}>
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<Router history={history}>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<FlyoutContextProvider>{children}</FlyoutContextProvider>
|
||||
</PackageInstallProvider>
|
||||
</Router>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={true} />
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<Router history={history}>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<FlyoutContextProvider>{children}</FlyoutContextProvider>
|
||||
</PackageInstallProvider>
|
||||
</Router>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
</QueryClientProvider>
|
||||
</EuiThemeProvider>
|
||||
</KibanaThemeProvider>
|
||||
</KibanaVersionContext.Provider>
|
||||
|
|
|
@ -10,7 +10,11 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { splitPkgKey } from '../../../../../../../common/services';
|
||||
|
||||
import { useGetPackageInfoByKey, useLink, useFleetServerHostsForPolicy } from '../../../../hooks';
|
||||
import {
|
||||
useGetPackageInfoByKeyQuery,
|
||||
useLink,
|
||||
useFleetServerHostsForPolicy,
|
||||
} from '../../../../hooks';
|
||||
|
||||
import type { AddToPolicyParams, CreatePackagePolicyParams } from '../types';
|
||||
|
||||
|
@ -70,7 +74,7 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({
|
|||
data: packageInfoData,
|
||||
error: packageInfoError,
|
||||
isLoading: isPackageInfoLoading,
|
||||
} = useGetPackageInfoByKey(pkgName, pkgVersion, { prerelease, full: true });
|
||||
} = useGetPackageInfoByKeyQuery(pkgName, pkgVersion, { prerelease, full: true });
|
||||
|
||||
const {
|
||||
agentPolicy,
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
sendGetAgentStatus,
|
||||
useIntraAppState,
|
||||
useStartServices,
|
||||
useGetPackageInfoByKey,
|
||||
useGetPackageInfoByKeyQuery,
|
||||
} from '../../../../hooks';
|
||||
|
||||
jest.mock('../../../../hooks', () => {
|
||||
|
@ -42,10 +42,10 @@ jest.mock('../../../../hooks', () => {
|
|||
sendGetOneAgentPolicy: jest.fn().mockResolvedValue({
|
||||
data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } },
|
||||
}),
|
||||
useGetPackageInfoByKeyQuery: jest.fn(),
|
||||
sendGetSettings: jest.fn().mockResolvedValue({
|
||||
data: { item: {} },
|
||||
}),
|
||||
useGetPackageInfoByKey: jest.fn(),
|
||||
sendCreatePackagePolicy: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { item: { id: 'policy-1', inputs: [] } } }),
|
||||
|
@ -186,7 +186,7 @@ describe('when on the package policy create page', () => {
|
|||
isLoading: false,
|
||||
};
|
||||
|
||||
(useGetPackageInfoByKey as jest.Mock).mockReturnValue(mockPackageInfo);
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(mockPackageInfo);
|
||||
});
|
||||
|
||||
describe('and Route state is provided via Fleet HashRouter', () => {
|
||||
|
@ -355,7 +355,7 @@ describe('when on the package policy create page', () => {
|
|||
});
|
||||
|
||||
test('should create agent policy without sys monitoring when new hosts is selected for system integration', async () => {
|
||||
(useGetPackageInfoByKey as jest.Mock).mockReturnValue({
|
||||
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue({
|
||||
...mockPackageInfo,
|
||||
data: {
|
||||
item: {
|
||||
|
|
|
@ -27,7 +27,7 @@ import { useCancelAddPackagePolicy } from '../hooks';
|
|||
import { splitPkgKey } from '../../../../../../../common/services';
|
||||
import { generateNewAgentPolicyWithDefaults } from '../../../../services';
|
||||
import type { NewAgentPolicy } from '../../../../types';
|
||||
import { useConfig, sendGetAgentStatus, useGetPackageInfoByKey } from '../../../../hooks';
|
||||
import { useConfig, sendGetAgentStatus, useGetPackageInfoByKeyQuery } from '../../../../hooks';
|
||||
import {
|
||||
Loading,
|
||||
Error as ErrorComponent,
|
||||
|
@ -97,7 +97,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
|
|||
data: packageInfoData,
|
||||
error: packageInfoError,
|
||||
isLoading: isPackageInfoLoading,
|
||||
} = useGetPackageInfoByKey(pkgName, pkgVersion, { full: true, prerelease });
|
||||
} = useGetPackageInfoByKeyQuery(pkgName, pkgVersion, { full: true, prerelease });
|
||||
const packageInfo = useMemo(() => {
|
||||
if (packageInfoData && packageInfoData.item) {
|
||||
return packageInfoData.item;
|
||||
|
|
|
@ -9,12 +9,12 @@ import React from 'react';
|
|||
|
||||
import { createFleetTestRendererMock } from '../../../../../../mock';
|
||||
import type { Agent, AgentPolicy } from '../../../../types';
|
||||
import { useGetPackageInfoByKey } from '../../../../../../hooks/use_request/epm';
|
||||
import { useGetPackageInfoByKeyQuery } from '../../../../../../hooks/use_request/epm';
|
||||
|
||||
import { AgentDashboardLink } from './agent_dashboard_link';
|
||||
|
||||
const mockedUseGetPackageInfoByKey = useGetPackageInfoByKey as jest.MockedFunction<
|
||||
typeof useGetPackageInfoByKey
|
||||
const mockedUseGetPackageInfoByKeyQuery = useGetPackageInfoByKeyQuery as jest.MockedFunction<
|
||||
typeof useGetPackageInfoByKeyQuery
|
||||
>;
|
||||
|
||||
jest.mock('../../../../../../hooks/use_fleet_status', () => ({
|
||||
|
@ -27,14 +27,14 @@ jest.mock('../../../../../../hooks/use_request/epm');
|
|||
|
||||
describe('AgentDashboardLink', () => {
|
||||
it('should enable the button if elastic_agent package is installed and policy has monitoring enabled', async () => {
|
||||
mockedUseGetPackageInfoByKey.mockReturnValue({
|
||||
mockedUseGetPackageInfoByKeyQuery.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
item: {
|
||||
status: 'installed',
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof useGetPackageInfoByKey>);
|
||||
} as ReturnType<typeof useGetPackageInfoByKeyQuery>);
|
||||
const testRenderer = createFleetTestRendererMock();
|
||||
|
||||
const result = testRenderer.render(
|
||||
|
@ -57,14 +57,14 @@ describe('AgentDashboardLink', () => {
|
|||
});
|
||||
|
||||
it('should not enable the button if elastic_agent package is not installed and policy has monitoring enabled', async () => {
|
||||
mockedUseGetPackageInfoByKey.mockReturnValue({
|
||||
mockedUseGetPackageInfoByKeyQuery.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
item: {
|
||||
status: 'not_installed',
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof useGetPackageInfoByKey>);
|
||||
} as ReturnType<typeof useGetPackageInfoByKeyQuery>);
|
||||
const testRenderer = createFleetTestRendererMock();
|
||||
|
||||
const result = testRenderer.render(
|
||||
|
@ -88,14 +88,14 @@ describe('AgentDashboardLink', () => {
|
|||
});
|
||||
|
||||
it('should link to the agent policy settings tab if logs and metrics are not enabled for that policy', async () => {
|
||||
mockedUseGetPackageInfoByKey.mockReturnValue({
|
||||
mockedUseGetPackageInfoByKeyQuery.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
item: {
|
||||
status: 'installed',
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof useGetPackageInfoByKey>);
|
||||
} as ReturnType<typeof useGetPackageInfoByKeyQuery>);
|
||||
const testRenderer = createFleetTestRendererMock();
|
||||
|
||||
const result = testRenderer.render(
|
||||
|
|
|
@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useGetPackageInfoByKey, useKibanaLink, useLink } from '../../../../hooks';
|
||||
import { useGetPackageInfoByKeyQuery, useKibanaLink, useLink } from '../../../../hooks';
|
||||
import type { Agent, AgentPolicy } from '../../../../types';
|
||||
import {
|
||||
FLEET_ELASTIC_AGENT_PACKAGE,
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
} from '../../../../../../../common/constants';
|
||||
|
||||
function useAgentDashboardLink(agent: Agent) {
|
||||
const { isLoading, data } = useGetPackageInfoByKey(FLEET_ELASTIC_AGENT_PACKAGE);
|
||||
const { isLoading, data } = useGetPackageInfoByKeyQuery(FLEET_ELASTIC_AGENT_PACKAGE);
|
||||
|
||||
const isInstalled = data?.item.status === 'installed';
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import { EuiErrorBoundary, EuiPortal } from '@elastic/eui';
|
|||
import type { History } from 'history';
|
||||
import { Router, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { KibanaContextProvider, RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
|
@ -39,6 +41,8 @@ import { PackageInstallProvider, UIExtensionsContext, FlyoutContextProvider } fr
|
|||
import { IntegrationsHeader } from './components/header';
|
||||
import { AgentEnrollmentFlyout } from './components';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const EmptyContext = () => <></>;
|
||||
|
||||
/**
|
||||
|
@ -79,28 +83,31 @@ export const IntegrationsAppContext: React.FC<{
|
|||
<KibanaVersionContext.Provider value={kibanaVersion}>
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<startServices.customIntegrations.ContextProvider>
|
||||
<CloudContext>
|
||||
<Router history={history}>
|
||||
<AgentPolicyContextProvider>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<FlyoutContextProvider>
|
||||
<IntegrationsHeader {...{ setHeaderActionMenu, theme$ }} />
|
||||
{children}
|
||||
<Chat />
|
||||
</FlyoutContextProvider>
|
||||
</PackageInstallProvider>
|
||||
</AgentPolicyContextProvider>
|
||||
</Router>
|
||||
</CloudContext>
|
||||
</startServices.customIntegrations.ContextProvider>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={true} />
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<startServices.customIntegrations.ContextProvider>
|
||||
<CloudContext>
|
||||
<Router history={history}>
|
||||
<AgentPolicyContextProvider>
|
||||
<PackageInstallProvider
|
||||
notifications={startServices.notifications}
|
||||
theme$={theme$}
|
||||
>
|
||||
<FlyoutContextProvider>
|
||||
<IntegrationsHeader {...{ setHeaderActionMenu, theme$ }} />
|
||||
{children}
|
||||
<Chat />
|
||||
</FlyoutContextProvider>
|
||||
</PackageInstallProvider>
|
||||
</AgentPolicyContextProvider>
|
||||
</Router>
|
||||
</CloudContext>
|
||||
</startServices.customIntegrations.ContextProvider>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
</QueryClientProvider>
|
||||
</EuiThemeProvider>
|
||||
</KibanaThemeProvider>
|
||||
</KibanaVersionContext.Provider>
|
||||
|
|
|
@ -14,5 +14,3 @@ export * from './use_agent_policy_context';
|
|||
export * from './use_integrations_state';
|
||||
export * from './use_confirm_force_install';
|
||||
export * from './use_confirm_open_unverified';
|
||||
export * from './use_packages';
|
||||
export * from './use_categories';
|
||||
|
|
|
@ -1,55 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
import type { RequestError } from '../../fleet/hooks';
|
||||
import { sendGetCategories } from '../../fleet/hooks';
|
||||
import type { GetCategoriesResponse } from '../types';
|
||||
|
||||
export function useCategories(prerelease?: boolean) {
|
||||
const [data, setData] = useState<GetCategoriesResponse | undefined>();
|
||||
const [error, setError] = useState<RequestError | undefined>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPrereleaseEnabled, setIsPrereleaseEnabled] = useState(prerelease);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (prerelease === undefined) {
|
||||
return;
|
||||
}
|
||||
if (isPrereleaseEnabled === prerelease) {
|
||||
return;
|
||||
}
|
||||
setIsPrereleaseEnabled(prerelease);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await sendGetCategories({
|
||||
include_policy_templates: true,
|
||||
prerelease,
|
||||
});
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
if (res.data) {
|
||||
setData(res.data);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [prerelease, isPrereleaseEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
}
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO: Refactor this away from constate, which is unmaintained, as this is the only
|
||||
// usage of it across the Fleet codebase
|
||||
import createContainer from 'constate';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
|
|
@ -1,56 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
import type { RequestError } from '../../fleet/hooks';
|
||||
import { sendGetPackages } from '../../fleet/hooks';
|
||||
import type { GetPackagesResponse } from '../types';
|
||||
|
||||
export function usePackages(prerelease?: boolean) {
|
||||
const [data, setData] = useState<GetPackagesResponse | undefined>();
|
||||
const [error, setError] = useState<RequestError | undefined>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPrereleaseEnabled, setIsPrereleaseEnabled] = useState(prerelease);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (prerelease === undefined) {
|
||||
return;
|
||||
}
|
||||
if (isPrereleaseEnabled === prerelease) {
|
||||
return;
|
||||
}
|
||||
setIsPrereleaseEnabled(prerelease);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await sendGetPackages({
|
||||
category: '',
|
||||
excludeInstallStatus: true,
|
||||
prerelease,
|
||||
});
|
||||
if (res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
if (res.data) {
|
||||
setData(res.data);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [prerelease, isPrereleaseEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
}
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useIsFirstTimeAgentUser } from './use_is_first_time_agent_user';
|
||||
export { useIsFirstTimeAgentUserQuery } from './use_is_first_time_agent_user';
|
||||
|
|
|
@ -5,50 +5,46 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { sendGetAgentPolicies, sendGetAgents } from '../../../../../hooks';
|
||||
import { useGetAgentPoliciesQuery, useGetAgentsQuery } from '../../../../../hooks';
|
||||
import { policyHasFleetServer } from '../../../../../services';
|
||||
|
||||
interface UseIsFirstTimeAgentUserResponse {
|
||||
isLoading: boolean;
|
||||
isFirstTimeAgentUser?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const useIsFirstTimeAgentUser = (): UseIsFirstTimeAgentUserResponse => {
|
||||
const [result, setResult] = useState<UseIsFirstTimeAgentUserResponse>({ isLoading: true });
|
||||
useEffect(() => {
|
||||
if (!result.isLoading) {
|
||||
return;
|
||||
}
|
||||
export const useIsFirstTimeAgentUserQuery = (): UseIsFirstTimeAgentUserResponse => {
|
||||
const {
|
||||
data: agentPolicies,
|
||||
isLoading: areAgentPoliciesLoading,
|
||||
isFetched: areAgentsFetched,
|
||||
} = useGetAgentPoliciesQuery({
|
||||
full: true,
|
||||
});
|
||||
|
||||
const getIsFirstTimeAgentUser = async () => {
|
||||
const { data: agentPoliciesData } = await sendGetAgentPolicies({
|
||||
full: true,
|
||||
});
|
||||
// now get all agents that are NOT part of a fleet server policy
|
||||
const serverPolicyIdsQuery = (agentPolicies?.items || [])
|
||||
.filter((item) => policyHasFleetServer(item))
|
||||
.map((p) => `policy_id:${p.id}`)
|
||||
.join(' or ');
|
||||
|
||||
// now get all agents that are NOT part of a fleet server policy
|
||||
const serverPolicyIdsQuery = (agentPoliciesData?.items || [])
|
||||
.filter((item) => policyHasFleetServer(item))
|
||||
.map((p) => `policy_id:${p.id}`)
|
||||
.join(' or ');
|
||||
// get agents that are not unenrolled and not fleet server
|
||||
const kuery =
|
||||
`not (_exists_:"unenrolled_at")` +
|
||||
(serverPolicyIdsQuery.length ? ` and not (${serverPolicyIdsQuery})` : '');
|
||||
|
||||
// get agents that are not unenrolled and not fleet server
|
||||
const kuery =
|
||||
`not (_exists_:"unenrolled_at")` +
|
||||
(serverPolicyIdsQuery.length ? ` and not (${serverPolicyIdsQuery})` : '');
|
||||
const { data: agents, isLoading: areAgentsLoading } = useGetAgentsQuery(
|
||||
{
|
||||
page: 1,
|
||||
perPage: 1, // we only need to know if there is at least one non-fleet agent
|
||||
showInactive: true,
|
||||
kuery,
|
||||
},
|
||||
{ enabled: areAgentsFetched } // don't run the query until agent policies are loaded
|
||||
);
|
||||
|
||||
const { data: agentStatusData } = await sendGetAgents({
|
||||
page: 1,
|
||||
perPage: 1, // we only need to know if there is at least one non-fleet agent
|
||||
showInactive: true,
|
||||
kuery,
|
||||
});
|
||||
setResult({ isLoading: false, isFirstTimeAgentUser: agentStatusData?.total === 0 });
|
||||
};
|
||||
|
||||
getIsFirstTimeAgentUser();
|
||||
}, [result]);
|
||||
|
||||
return result;
|
||||
return {
|
||||
isLoading: areAgentPoliciesLoading || areAgentsLoading,
|
||||
isFirstTimeAgentUser: agents?.data?.total === 0,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -39,14 +39,14 @@ import {
|
|||
useBreadcrumbs,
|
||||
useStartServices,
|
||||
useAuthz,
|
||||
usePermissionCheck,
|
||||
usePermissionCheckQuery,
|
||||
useIntegrationsStateContext,
|
||||
useGetSettings,
|
||||
useGetSettingsQuery,
|
||||
} from '../../../../hooks';
|
||||
import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants';
|
||||
import { ExperimentalFeaturesService } from '../../../../services';
|
||||
import {
|
||||
useGetPackageInfoByKey,
|
||||
useGetPackageInfoByKeyQuery,
|
||||
useLink,
|
||||
useAgentPolicyContext,
|
||||
useIsGuidedOnboardingActive,
|
||||
|
@ -63,7 +63,7 @@ import {
|
|||
import type { WithHeaderLayoutProps } from '../../../../layouts';
|
||||
import { WithHeaderLayout } from '../../../../layouts';
|
||||
|
||||
import { useIsFirstTimeAgentUser } from './hooks';
|
||||
import { useIsFirstTimeAgentUserQuery } from './hooks';
|
||||
import { getInstallPkgRouteOptions } from './utils';
|
||||
import {
|
||||
IntegrationAgentPolicyCount,
|
||||
|
@ -114,17 +114,24 @@ export function Detail() {
|
|||
const { getFromIntegrations } = useIntegrationsStateContext();
|
||||
const { pkgkey, panel } = useParams<DetailParams>();
|
||||
const { getHref, getPath } = useLink();
|
||||
const canInstallPackages = useAuthz().integrations.installPackages;
|
||||
const canReadPackageSettings = useAuthz().integrations.readPackageSettings;
|
||||
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
|
||||
const permissionCheck = usePermissionCheck();
|
||||
const missingSecurityConfiguration =
|
||||
!permissionCheck.data?.success && permissionCheck.data?.error === 'MISSING_SECURITY';
|
||||
const userCanInstallPackages = canInstallPackages && permissionCheck.data?.success;
|
||||
const history = useHistory();
|
||||
const { pathname, search, hash } = useLocation();
|
||||
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
const integration = useMemo(() => queryParams.get('integration'), [queryParams]);
|
||||
|
||||
const canInstallPackages = useAuthz().integrations.installPackages;
|
||||
const canReadPackageSettings = useAuthz().integrations.readPackageSettings;
|
||||
const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies;
|
||||
|
||||
const {
|
||||
data: permissionCheck,
|
||||
error: permissionCheckError,
|
||||
isLoading: isPermissionCheckLoading,
|
||||
} = usePermissionCheckQuery();
|
||||
const missingSecurityConfiguration =
|
||||
!permissionCheck?.success && permissionCheckError === 'MISSING_SECURITY';
|
||||
const userCanInstallPackages = canInstallPackages && permissionCheck?.success;
|
||||
|
||||
const services = useStartServices();
|
||||
const isCloud = !!services?.cloud?.cloudId;
|
||||
const { createPackagePolicyMultiPageLayout: isExperimentalAddIntegrationPageEnabled } =
|
||||
|
@ -163,7 +170,7 @@ export function Detail() {
|
|||
boolean | undefined
|
||||
>();
|
||||
|
||||
const { data: settings } = useGetSettings();
|
||||
const { data: settings } = useGetSettingsQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const isEnabled = Boolean(settings?.item.prerelease_integrations_enabled);
|
||||
|
@ -176,9 +183,8 @@ export function Detail() {
|
|||
data: packageInfoData,
|
||||
error: packageInfoError,
|
||||
isLoading: packageInfoLoading,
|
||||
isInitialRequest: packageIsInitialRequest,
|
||||
resendRequest: refreshPackageInfo,
|
||||
} = useGetPackageInfoByKey(pkgName, pkgVersion, {
|
||||
refetch: refetchPackageInfo,
|
||||
} = useGetPackageInfoByKeyQuery(pkgName, pkgVersion, {
|
||||
prerelease: prereleaseIntegrationsEnabled,
|
||||
});
|
||||
|
||||
|
@ -186,7 +192,7 @@ export function Detail() {
|
|||
const [latestPrereleaseVersion, setLatestPrereleaseVersion] = useState<string | undefined>();
|
||||
|
||||
// fetch latest GA version (prerelease=false)
|
||||
const { data: packageInfoLatestGAData } = useGetPackageInfoByKey(pkgName, '', {
|
||||
const { data: packageInfoLatestGAData } = useGetPackageInfoByKeyQuery(pkgName, '', {
|
||||
prerelease: false,
|
||||
});
|
||||
|
||||
|
@ -199,7 +205,7 @@ export function Detail() {
|
|||
}, [packageInfoLatestGAData?.item]);
|
||||
|
||||
// fetch latest Prerelease version (prerelease=true)
|
||||
const { data: packageInfoLatestPrereleaseData } = useGetPackageInfoByKey(pkgName, '', {
|
||||
const { data: packageInfoLatestPrereleaseData } = useGetPackageInfoByKeyQuery(pkgName, '', {
|
||||
prerelease: true,
|
||||
});
|
||||
|
||||
|
@ -208,7 +214,7 @@ export function Detail() {
|
|||
}, [packageInfoLatestPrereleaseData?.item.version]);
|
||||
|
||||
const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } =
|
||||
useIsFirstTimeAgentUser();
|
||||
useIsFirstTimeAgentUserQuery();
|
||||
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(pkgName);
|
||||
|
||||
// Refresh package info when status change
|
||||
|
@ -220,17 +226,14 @@ export function Detail() {
|
|||
}
|
||||
if (oldPackageInstallStatus === 'not_installed' && packageInstallStatus === 'installed') {
|
||||
setOldPackageStatus(packageInstallStatus);
|
||||
refreshPackageInfo();
|
||||
refetchPackageInfo();
|
||||
}
|
||||
}, [packageInstallStatus, oldPackageInstallStatus, refreshPackageInfo]);
|
||||
}, [packageInstallStatus, oldPackageInstallStatus, refetchPackageInfo]);
|
||||
|
||||
const isLoading =
|
||||
(packageInfoLoading && !packageIsInitialRequest) ||
|
||||
permissionCheck.isLoading ||
|
||||
firstTimeUserLoading;
|
||||
const isLoading = packageInfoLoading || isPermissionCheckLoading || firstTimeUserLoading;
|
||||
|
||||
const showCustomTab =
|
||||
useUIExtension(packageInfoData?.item.name ?? '', 'package-detail-custom') !== undefined;
|
||||
useUIExtension(packageInfoData?.item?.name ?? '', 'package-detail-custom') !== undefined;
|
||||
|
||||
// Track install status state
|
||||
useEffect(() => {
|
||||
|
@ -698,7 +701,7 @@ export function Detail() {
|
|||
defaultMessage="Error loading integration details"
|
||||
/>
|
||||
}
|
||||
error={packageInfoError}
|
||||
error={packageInfoError.message}
|
||||
/>
|
||||
) : isLoading || !packageInfo ? (
|
||||
<Loading />
|
||||
|
|
|
@ -29,7 +29,7 @@ import type {
|
|||
KibanaAssetType,
|
||||
} from '../../../../../types';
|
||||
import { entries } from '../../../../../types';
|
||||
import { useGetCategories } from '../../../../../hooks';
|
||||
import { useGetCategoriesQuery } from '../../../../../hooks';
|
||||
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
|
||||
|
||||
import { NoticeModal } from './notice_modal';
|
||||
|
@ -59,10 +59,10 @@ const Replacements = euiStyled(EuiFlexItem)`
|
|||
`;
|
||||
|
||||
export const Details: React.FC<Props> = memo(({ packageInfo }) => {
|
||||
const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories();
|
||||
const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategoriesQuery();
|
||||
const packageCategories: string[] = useMemo(() => {
|
||||
if (!isLoadingCategories && categoriesData && categoriesData.response) {
|
||||
return categoriesData.response
|
||||
if (!isLoadingCategories && categoriesData?.items) {
|
||||
return categoriesData.items
|
||||
.filter((category) => packageInfo.categories?.includes(category.id as PackageSpecCategory))
|
||||
.map((category) => category.title);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiCodeBlock,
|
||||
EuiLoadingContent,
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { sendGetFileByPath, useStartServices } from '../../../../../hooks';
|
||||
import { useGetFileByPathQuery, useStartServices } from '../../../../../hooks';
|
||||
|
||||
interface Props {
|
||||
licenseName?: string;
|
||||
|
@ -33,23 +33,17 @@ export const LicenseModal: React.FunctionComponent<Props> = ({
|
|||
onClose,
|
||||
}) => {
|
||||
const { notifications } = useStartServices();
|
||||
const [licenseText, setLicenseText] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const { data } = await sendGetFileByPath(licensePath);
|
||||
setLicenseText(data || '');
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.fleet.epm.errorLoadingLicense', {
|
||||
defaultMessage: 'Error loading license information',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [licensePath, notifications]);
|
||||
const { data: licenseText, error: licenseError } = useGetFileByPathQuery(licensePath);
|
||||
|
||||
if (licenseError) {
|
||||
notifications.toasts.addError(licenseError, {
|
||||
title: i18n.translate('xpack.fleet.epm.errorLoadingLicense', {
|
||||
defaultMessage: 'Error loading license information',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiModal maxWidth={true} onClose={onClose}>
|
||||
<EuiModalHeader>
|
||||
|
|
|
@ -99,6 +99,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
|
|||
const getPackageInstallStatus = useGetPackageInstallStatus();
|
||||
const packageInstallStatus = getPackageInstallStatus(name);
|
||||
const { pagination, pageSizeOptions, setPagination } = useUrlPagination();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import semverLt from 'semver/functions/lt';
|
||||
|
@ -25,15 +25,15 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { Observable } from 'rxjs';
|
||||
import type { CoreTheme } from '@kbn/core/public';
|
||||
|
||||
import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../../../types';
|
||||
import type { PackageInfo } from '../../../../../types';
|
||||
import { InstallStatus } from '../../../../../types';
|
||||
import {
|
||||
useGetPackagePolicies,
|
||||
useGetPackagePoliciesQuery,
|
||||
useGetPackageInstallStatus,
|
||||
useLink,
|
||||
sendUpgradePackagePolicyDryRun,
|
||||
sendUpdatePackage,
|
||||
useStartServices,
|
||||
useUpgradePackagePolicyDryRunQuery,
|
||||
useUpdatePackageMutation,
|
||||
} from '../../../../../hooks';
|
||||
import {
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
|
@ -100,15 +100,30 @@ interface Props {
|
|||
|
||||
export const SettingsPage: React.FC<Props> = memo(({ packageInfo, theme$ }: Props) => {
|
||||
const { name, title, latestVersion, version, keepPoliciesUpToDate } = packageInfo;
|
||||
const [dryRunData, setDryRunData] = useState<UpgradePackagePolicyDryRunResponse | null>();
|
||||
const [isUpgradingPackagePolicies, setIsUpgradingPackagePolicies] = useState<boolean>(false);
|
||||
const getPackageInstallStatus = useGetPackageInstallStatus();
|
||||
const { data: packagePoliciesData } = useGetPackagePolicies({
|
||||
|
||||
const { data: packagePoliciesData } = useGetPackagePoliciesQuery({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
page: 1,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`,
|
||||
});
|
||||
|
||||
const packagePolicyIds = useMemo(
|
||||
() => packagePoliciesData?.items.map(({ id }) => id),
|
||||
[packagePoliciesData]
|
||||
);
|
||||
|
||||
const { data: dryRunData } = useUpgradePackagePolicyDryRunQuery(
|
||||
packagePolicyIds ?? [],
|
||||
latestVersion,
|
||||
{
|
||||
enabled: packagePolicyIds && packagePolicyIds.length > 0,
|
||||
}
|
||||
);
|
||||
|
||||
const updatePackageMutation = useUpdatePackageMutation();
|
||||
|
||||
const { notifications } = useStartServices();
|
||||
|
||||
const shouldShowKeepPoliciesUpToDateSwitch = useMemo(() => {
|
||||
|
@ -124,72 +139,60 @@ export const SettingsPage: React.FC<Props> = memo(({ packageInfo, theme$ }: Prop
|
|||
);
|
||||
|
||||
const handleKeepPoliciesUpToDateSwitchChange = useCallback(() => {
|
||||
const saveKeepPoliciesUpToDate = async () => {
|
||||
try {
|
||||
setKeepPoliciesUpToDateSwitchValue((prev) => !prev);
|
||||
setKeepPoliciesUpToDateSwitchValue((prev) => !prev);
|
||||
|
||||
await sendUpdatePackage(packageInfo.name, packageInfo.version, {
|
||||
updatePackageMutation.mutate(
|
||||
{
|
||||
pkgName: packageInfo.name,
|
||||
pkgVersion: packageInfo.version,
|
||||
body: {
|
||||
keepPoliciesUpToDate: !keepPoliciesUpToDateSwitchValue,
|
||||
});
|
||||
|
||||
notifications.toasts.addSuccess({
|
||||
title: i18n.translate('xpack.fleet.integrations.integrationSaved', {
|
||||
defaultMessage: 'Integration settings saved',
|
||||
}),
|
||||
text: !keepPoliciesUpToDateSwitchValue
|
||||
? i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateEnabledSuccess', {
|
||||
defaultMessage:
|
||||
'Fleet will automatically keep integration policies up to date for {title}',
|
||||
values: { title },
|
||||
})
|
||||
: i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateDisabledSuccess', {
|
||||
defaultMessage:
|
||||
'Fleet will not automatically keep integration policies up to date for {title}',
|
||||
values: { title },
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.toasts.addError(error, {
|
||||
title: i18n.translate('xpack.fleet.integrations.integrationSavedError', {
|
||||
defaultMessage: 'Error saving integration settings',
|
||||
}),
|
||||
toastMessage: i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateError', {
|
||||
defaultMessage: 'Error saving integration settings for {title}',
|
||||
values: { title },
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.toasts.addSuccess({
|
||||
title: i18n.translate('xpack.fleet.integrations.integrationSaved', {
|
||||
defaultMessage: 'Integration settings saved',
|
||||
}),
|
||||
text: !keepPoliciesUpToDateSwitchValue
|
||||
? i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateEnabledSuccess', {
|
||||
defaultMessage:
|
||||
'Fleet will automatically keep integration policies up to date for {title}',
|
||||
values: { title },
|
||||
})
|
||||
: i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateDisabledSuccess', {
|
||||
defaultMessage:
|
||||
'Fleet will not automatically keep integration policies up to date for {title}',
|
||||
values: { title },
|
||||
}),
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.toasts.addError(error, {
|
||||
title: i18n.translate('xpack.fleet.integrations.integrationSavedError', {
|
||||
defaultMessage: 'Error saving integration settings',
|
||||
}),
|
||||
toastMessage: i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateError', {
|
||||
defaultMessage: 'Error saving integration settings for {title}',
|
||||
values: { title },
|
||||
}),
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
saveKeepPoliciesUpToDate();
|
||||
);
|
||||
}, [
|
||||
keepPoliciesUpToDateSwitchValue,
|
||||
notifications.toasts,
|
||||
packageInfo.name,
|
||||
packageInfo.version,
|
||||
title,
|
||||
updatePackageMutation,
|
||||
]);
|
||||
|
||||
const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name);
|
||||
const packageHasUsages = !!packagePoliciesData?.total;
|
||||
|
||||
const packagePolicyIds = useMemo(
|
||||
() => packagePoliciesData?.items.map(({ id }) => id),
|
||||
[packagePoliciesData]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDryRunData = async () => {
|
||||
if (packagePolicyIds && packagePolicyIds.length) {
|
||||
const { data } = await sendUpgradePackagePolicyDryRun(packagePolicyIds, latestVersion);
|
||||
|
||||
setDryRunData(data);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDryRunData();
|
||||
}, [latestVersion, packagePolicyIds]);
|
||||
|
||||
const updateAvailable =
|
||||
installedVersion && semverLt(installedVersion, latestVersion) ? true : false;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -24,7 +24,6 @@ import type { CoreTheme } from '@kbn/core/public';
|
|||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type {
|
||||
GetAgentPoliciesResponse,
|
||||
PackageInfo,
|
||||
UpgradePackagePolicyDryRunResponse,
|
||||
PackagePolicy,
|
||||
|
@ -32,13 +31,13 @@ import type {
|
|||
import { InstallStatus } from '../../../../../types';
|
||||
import { AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../../../constants';
|
||||
import {
|
||||
sendGetAgentPolicies,
|
||||
useInstallPackage,
|
||||
useGetPackageInstallStatus,
|
||||
sendUpgradePackagePolicy,
|
||||
useStartServices,
|
||||
useAuthz,
|
||||
useLink,
|
||||
useUpgradePackagePoliciesMutation,
|
||||
useGetAgentPoliciesQuery,
|
||||
} from '../../../../../hooks';
|
||||
|
||||
interface UpdateButtonProps extends Pick<PackageInfo, 'name' | 'title' | 'version'> {
|
||||
|
@ -92,26 +91,17 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
|
|||
|
||||
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false);
|
||||
const [upgradePackagePolicies, setUpgradePackagePolicies] = useState<boolean>(true);
|
||||
const [agentPolicyData, setAgentPolicyData] = useState<GetAgentPoliciesResponse | null>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgentPolicyData = async () => {
|
||||
if (packagePolicyIds && packagePolicyIds.length > 0) {
|
||||
const { data } = await sendGetAgentPolicies({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
page: 1,
|
||||
// Fetch all agent policies that include one of the eligible package policies
|
||||
kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:${packagePolicyIds
|
||||
.map((id) => `"${id}"`)
|
||||
.join(' or ')}`,
|
||||
});
|
||||
|
||||
setAgentPolicyData(data);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgentPolicyData();
|
||||
}, [packagePolicyIds]);
|
||||
const { data: agentPolicyData } = useGetAgentPoliciesQuery({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
page: 1,
|
||||
// Fetch all agent policies that include one of the eligible package policies
|
||||
kuery: packagePolicyIds.length
|
||||
? `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:${packagePolicyIds
|
||||
.map((id) => `"${id}"`)
|
||||
.join(' or ')}`
|
||||
: '',
|
||||
});
|
||||
|
||||
const packagePolicyCount = useMemo(() => packagePolicyIds.length, [packagePolicyIds]);
|
||||
|
||||
|
@ -164,6 +154,8 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
|
|||
await installPackage({ name, version, title, fromUpdate: true });
|
||||
}, [installPackage, name, title, version]);
|
||||
|
||||
const upgradePackagePoliciesMutation = useUpgradePackagePoliciesMutation();
|
||||
|
||||
const handleClickUpgradePolicies = useCallback(async () => {
|
||||
if (isUpgradingPackagePolicies) {
|
||||
return;
|
||||
|
@ -174,47 +166,51 @@ export const UpdateButton: React.FunctionComponent<UpdateButtonProps> = ({
|
|||
|
||||
await installPackage({ name, version, title });
|
||||
|
||||
await sendUpgradePackagePolicy(
|
||||
// Only upgrade policies that don't have conflicts
|
||||
packagePolicyIds.filter(
|
||||
(id) => !dryRunData?.find((dryRunRecord) => dryRunRecord.diff?.[0].id === id)?.hasErrors
|
||||
)
|
||||
upgradePackagePoliciesMutation.mutate(
|
||||
{
|
||||
// Only upgrade policies that don't have conflicts
|
||||
packagePolicyIds: packagePolicyIds.filter(
|
||||
(id) => !dryRunData?.find((dryRunRecord) => dryRunRecord.diff?.[0].id === id)?.hasErrors
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.toasts.addSuccess({
|
||||
title: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessTitle"
|
||||
defaultMessage="Updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessDescription"
|
||||
defaultMessage="Successfully updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
});
|
||||
|
||||
navigateToNewSettingsPage();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setIsUpgradingPackagePolicies(false);
|
||||
|
||||
notifications.toasts.addSuccess({
|
||||
title: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessTitle"
|
||||
defaultMessage="Updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
text: toMountPoint(
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.packageUpdateSuccessDescription"
|
||||
defaultMessage="Successfully updated {title} and upgraded policies"
|
||||
values={{ title }}
|
||||
/>,
|
||||
{ theme$ }
|
||||
),
|
||||
});
|
||||
|
||||
navigateToNewSettingsPage();
|
||||
}, [
|
||||
dryRunData,
|
||||
installPackage,
|
||||
isUpgradingPackagePolicies,
|
||||
name,
|
||||
navigateToNewSettingsPage,
|
||||
notifications.toasts,
|
||||
packagePolicyIds,
|
||||
setIsUpgradingPackagePolicies,
|
||||
title,
|
||||
installPackage,
|
||||
name,
|
||||
version,
|
||||
title,
|
||||
upgradePackagePoliciesMutation,
|
||||
packagePolicyIds,
|
||||
dryRunData,
|
||||
notifications.toasts,
|
||||
theme$,
|
||||
navigateToNewSettingsPage,
|
||||
]);
|
||||
|
||||
const updateModal = (
|
||||
|
|
|
@ -11,7 +11,7 @@ import { uniq, xorBy } from 'lodash';
|
|||
import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common';
|
||||
|
||||
import type { IntegrationPreferenceType } from '../../../components/integration_preference';
|
||||
import { usePackages, useCategories } from '../../../../../hooks';
|
||||
import { useGetPackagesQuery, useGetCategoriesQuery } from '../../../../../hooks';
|
||||
import {
|
||||
useGetAppendCustomIntegrations,
|
||||
useGetReplacementCustomIntegrations,
|
||||
|
@ -130,7 +130,7 @@ export const useAvailablePackages = () => {
|
|||
data: eprPackages,
|
||||
isLoading: isLoadingAllPackages,
|
||||
error: eprPackageLoadingError,
|
||||
} = usePackages(prereleaseIntegrationsEnabled);
|
||||
} = useGetPackagesQuery({ prerelease: prereleaseIntegrationsEnabled });
|
||||
|
||||
// Remove Kubernetes package granularity
|
||||
if (eprPackages?.items) {
|
||||
|
@ -185,7 +185,7 @@ export const useAvailablePackages = () => {
|
|||
data: eprCategoriesRes,
|
||||
isLoading: isLoadingCategories,
|
||||
error: eprCategoryLoadingError,
|
||||
} = useCategories(prereleaseIntegrationsEnabled);
|
||||
} = useGetCategoriesQuery({ prerelease: prereleaseIntegrationsEnabled });
|
||||
|
||||
const eprCategories = useMemo(() => eprCategoriesRes?.items || [], [eprCategoriesRes]);
|
||||
// Subcategories
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common';
|
||||
|
@ -26,7 +26,7 @@ import type {
|
|||
IntegrationCardReleaseLabel,
|
||||
} from '../../../../../../../common/types/models';
|
||||
|
||||
import { useGetPackages } from '../../../../hooks';
|
||||
import { useGetPackagesQuery } from '../../../../hooks';
|
||||
|
||||
import type { CategoryFacet, ExtendedIntegrationCategory } from './category_facets';
|
||||
|
||||
|
@ -115,14 +115,12 @@ export const EPMHomePage: React.FC = () => {
|
|||
const [prereleaseEnabled, setPrereleaseEnabled] = useState<boolean>(false);
|
||||
|
||||
// loading packages to find installed ones
|
||||
const { data: allPackages, isLoading } = useGetPackages({
|
||||
const { data: allPackages, isLoading } = useGetPackagesQuery({
|
||||
prerelease: prereleaseEnabled,
|
||||
});
|
||||
|
||||
const installedPackages = useMemo(
|
||||
() =>
|
||||
(allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed),
|
||||
[allPackages?.response]
|
||||
const installedPackages = (allPackages?.items || []).filter(
|
||||
(pkg) => pkg.status === installationStatuses.Installed
|
||||
);
|
||||
|
||||
const unverifiedPackageCount = installedPackages.filter(
|
||||
|
|
|
@ -10,17 +10,17 @@ import { useRouteMatch } from 'react-router-dom';
|
|||
|
||||
// TODO: Needs to be moved
|
||||
import { EditPackagePolicyForm } from '../../../../../fleet/sections/agent_policy/edit_package_policy_page';
|
||||
import { useGetOnePackagePolicy, useUIExtension } from '../../../../hooks';
|
||||
import { useGetOnePackagePolicyQuery, useUIExtension } from '../../../../hooks';
|
||||
|
||||
export const Policy = memo(() => {
|
||||
const {
|
||||
params: { packagePolicyId },
|
||||
} = useRouteMatch<{ packagePolicyId: string }>();
|
||||
|
||||
const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
|
||||
const { data: packagePolicyData } = useGetOnePackagePolicyQuery(packagePolicyId);
|
||||
|
||||
const extensionView = useUIExtension(
|
||||
packagePolicy.data?.item?.package?.name ?? '',
|
||||
packagePolicyData?.item?.package?.name ?? '',
|
||||
'package-policy-edit'
|
||||
);
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ export const FleetStatusProvider: React.FC = ({ children }) => {
|
|||
isReady: false,
|
||||
});
|
||||
|
||||
// TODO: Refactor to use react-query
|
||||
const sendGetStatus = useCallback(
|
||||
async function sendGetStatus() {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import semverLt from 'semver/functions/lt';
|
||||
|
||||
import { installationStatuses } from '../../common/constants';
|
||||
import type { PackagePolicy } from '../types';
|
||||
|
||||
import { useGetPackagesQuery } from './use_request/epm';
|
||||
import { useGetAgentPoliciesQuery } from './use_request/agent_policy';
|
||||
|
||||
interface UpdatableIntegration {
|
||||
currentVersion: string;
|
||||
policiesToUpgrade: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
agentsCount: number;
|
||||
pkgPolicyId: string;
|
||||
pkgPolicyName: string;
|
||||
pkgPolicyIntegrationVersion: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const usePackageInstallationsQuery = () => {
|
||||
const { data: allPackages, isLoading: isLoadingPackages } = useGetPackagesQuery({
|
||||
prerelease: true,
|
||||
});
|
||||
|
||||
const { data: agentPolicyData, isLoading: isLoadingPolicies } = useGetAgentPoliciesQuery({
|
||||
full: true,
|
||||
});
|
||||
|
||||
const allInstalledPackages = useMemo(
|
||||
() => (allPackages?.items || []).filter((pkg) => pkg.status === installationStatuses.Installed),
|
||||
[allPackages?.items]
|
||||
);
|
||||
|
||||
const updatablePackages = useMemo(
|
||||
() =>
|
||||
allInstalledPackages.filter(
|
||||
(item) =>
|
||||
'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version)
|
||||
),
|
||||
[allInstalledPackages]
|
||||
);
|
||||
|
||||
const updatableIntegrations = useMemo<Map<string, UpdatableIntegration>>(
|
||||
() =>
|
||||
(agentPolicyData?.items || []).reduce((result, policy) => {
|
||||
policy.package_policies?.forEach((pkgPolicy: PackagePolicy) => {
|
||||
if (!pkgPolicy.package) return false;
|
||||
const { name, version } = pkgPolicy.package;
|
||||
const installedPackage = allInstalledPackages.find(
|
||||
(installedPkg) =>
|
||||
'savedObject' in installedPkg && installedPkg.savedObject.attributes.name === name
|
||||
);
|
||||
if (
|
||||
installedPackage &&
|
||||
'savedObject' in installedPackage &&
|
||||
semverLt(version, installedPackage.savedObject.attributes.version)
|
||||
) {
|
||||
const packageData = result.get(name) ?? {
|
||||
currentVersion: installedPackage.savedObject.attributes.version,
|
||||
policiesToUpgrade: [],
|
||||
};
|
||||
packageData.policiesToUpgrade.push({
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
agentsCount: policy.agents ?? 0,
|
||||
pkgPolicyId: pkgPolicy.id,
|
||||
pkgPolicyName: pkgPolicy.name,
|
||||
pkgPolicyIntegrationVersion: version,
|
||||
});
|
||||
result.set(name, packageData);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, new Map<string, UpdatableIntegration>()),
|
||||
[allInstalledPackages, agentPolicyData]
|
||||
);
|
||||
|
||||
return {
|
||||
allPackages,
|
||||
allInstalledPackages,
|
||||
updatablePackages,
|
||||
updatableIntegrations,
|
||||
isLoadingPackages,
|
||||
isLoadingPolicies,
|
||||
};
|
||||
};
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { agentPolicyRouteService } from '../../services';
|
||||
|
||||
|
@ -22,8 +23,8 @@ import type {
|
|||
DeleteAgentPolicyResponse,
|
||||
} from '../../types';
|
||||
|
||||
import { useRequest, sendRequest, useConditionalRequest } from './use_request';
|
||||
import type { SendConditionalRequestConfig } from './use_request';
|
||||
import { useRequest, sendRequest, useConditionalRequest, sendRequestForRq } from './use_request';
|
||||
import type { SendConditionalRequestConfig, RequestError } from './use_request';
|
||||
|
||||
export const useGetAgentPolicies = (query?: GetAgentPoliciesRequest['query']) => {
|
||||
return useRequest<GetAgentPoliciesResponse>({
|
||||
|
@ -33,6 +34,16 @@ export const useGetAgentPolicies = (query?: GetAgentPoliciesRequest['query']) =>
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetAgentPoliciesQuery = (query?: GetAgentPoliciesRequest['query']) => {
|
||||
return useQuery<GetAgentPoliciesResponse, RequestError>(['agentPolicies', query], () =>
|
||||
sendRequestForRq<GetAgentPoliciesResponse>({
|
||||
path: agentPolicyRouteService.getListPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const sendGetAgentPolicies = (query?: GetAgentPoliciesRequest['query']) => {
|
||||
return sendRequest<GetAgentPoliciesResponse>({
|
||||
path: agentPolicyRouteService.getListPath(),
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
GetActionStatusResponse,
|
||||
|
@ -72,6 +73,13 @@ export function useGetAgents(query: GetAgentsRequest['query'], options?: Request
|
|||
});
|
||||
}
|
||||
|
||||
export function useGetAgentsQuery(
|
||||
query: GetAgentsRequest['query'],
|
||||
options: Partial<{ enabled: boolean }> = {}
|
||||
) {
|
||||
return useQuery(['agents', query], () => sendGetAgents(query), { enabled: options.enabled });
|
||||
}
|
||||
|
||||
export function sendGetAgents(query: GetAgentsRequest['query'], options?: RequestOptions) {
|
||||
return sendRequest<GetAgentsResponse>({
|
||||
method: 'get',
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { appRoutesService } from '../../services';
|
||||
import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../types';
|
||||
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
import { sendRequest, sendRequestForRq, useRequest } from './use_request';
|
||||
|
||||
export const sendGetPermissionsCheck = (fleetServerSetup?: boolean) => {
|
||||
return sendRequest<CheckPermissionsResponse>({
|
||||
|
@ -25,6 +27,17 @@ export const sendGenerateServiceToken = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const usePermissionCheckQuery = () => {
|
||||
return useQuery<CheckPermissionsResponse, CheckPermissionsResponse['error']>(
|
||||
['permissionsCheck'],
|
||||
() =>
|
||||
sendRequestForRq<CheckPermissionsResponse>({
|
||||
path: appRoutesService.getCheckPermissionsPath(),
|
||||
method: 'get',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const usePermissionCheck = () => {
|
||||
return useRequest<CheckPermissionsResponse>({
|
||||
path: appRoutesService.getCheckPermissionsPath(),
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
*/
|
||||
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import { epmRouteService, isVerificationError } from '../../services';
|
||||
import type {
|
||||
|
@ -28,7 +31,8 @@ import { getCustomIntegrations } from '../../services/custom_integrations';
|
|||
|
||||
import { useConfirmOpenUnverified } from '../../applications/integrations/hooks/use_confirm_open_unverified';
|
||||
|
||||
import { useRequest, sendRequest } from './use_request';
|
||||
import type { RequestError } from './use_request';
|
||||
import { useRequest, sendRequest, sendRequestForRq } from './use_request';
|
||||
|
||||
export function useGetAppendCustomIntegrations() {
|
||||
const customIntegrations = getCustomIntegrations();
|
||||
|
@ -40,13 +44,15 @@ export function useGetReplacementCustomIntegrations() {
|
|||
return useAsync(customIntegrations.getReplacementCustomIntegrations, []);
|
||||
}
|
||||
|
||||
export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
||||
return useRequest<GetCategoriesResponse>({
|
||||
path: epmRouteService.getCategoriesPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
});
|
||||
};
|
||||
export function useGetCategoriesQuery(query: GetCategoriesRequest['query'] = {}) {
|
||||
return useQuery<GetCategoriesResponse, RequestError>(['categories', query], () =>
|
||||
sendRequestForRq<GetCategoriesResponse>({
|
||||
path: epmRouteService.getCategoriesPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const sendGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
|
||||
return sendRequest<GetCategoriesResponse>({
|
||||
|
@ -64,6 +70,16 @@ export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetPackagesQuery = (query: GetPackagesRequest['query']) => {
|
||||
return useQuery<GetPackagesResponse, RequestError>(['get-packages', query], () =>
|
||||
sendRequestForRq<GetPackagesResponse>({
|
||||
path: epmRouteService.getListPath(),
|
||||
method: 'get',
|
||||
query,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const sendGetPackages = (query: GetPackagesRequest['query'] = {}) => {
|
||||
return sendRequest<GetPackagesResponse>({
|
||||
path: epmRouteService.getListPath(),
|
||||
|
@ -79,7 +95,7 @@ export const useGetLimitedPackages = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetPackageInfoByKey = (
|
||||
export const useGetPackageInfoByKeyQuery = (
|
||||
pkgName: string,
|
||||
pkgVersion?: string,
|
||||
options?: {
|
||||
|
@ -92,30 +108,31 @@ export const useGetPackageInfoByKey = (
|
|||
const [ignoreUnverifiedQueryParam, setIgnoreUnverifiedQueryParam] = useState(
|
||||
options?.ignoreUnverified
|
||||
);
|
||||
const res = useRequest<GetInfoResponse>({
|
||||
path: epmRouteService.getInfoPath(pkgName, pkgVersion),
|
||||
method: 'get',
|
||||
query: {
|
||||
...options,
|
||||
...(ignoreUnverifiedQueryParam && { ignoreUnverified: ignoreUnverifiedQueryParam }),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const confirm = async () => {
|
||||
const forceInstall = await confirmOpenUnverified(pkgName);
|
||||
const response = useQuery<GetInfoResponse, RequestError>([pkgName, pkgVersion, options], () =>
|
||||
sendRequestForRq<GetInfoResponse>({
|
||||
path: epmRouteService.getInfoPath(pkgName, pkgVersion),
|
||||
method: 'get',
|
||||
query: {
|
||||
...options,
|
||||
...(ignoreUnverifiedQueryParam && { ignoreUnverified: ignoreUnverifiedQueryParam }),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (forceInstall) {
|
||||
setIgnoreUnverifiedQueryParam(true);
|
||||
}
|
||||
};
|
||||
const confirm = async () => {
|
||||
const forceInstall = await confirmOpenUnverified(pkgName);
|
||||
|
||||
if (res.error && isVerificationError(res.error)) {
|
||||
confirm();
|
||||
if (forceInstall) {
|
||||
setIgnoreUnverifiedQueryParam(true);
|
||||
}
|
||||
}, [res.error, pkgName, pkgVersion, confirmOpenUnverified]);
|
||||
};
|
||||
|
||||
return res;
|
||||
if (response?.error && isVerificationError(response?.error)) {
|
||||
confirm();
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const useGetPackageStats = (pkgName: string) => {
|
||||
|
@ -148,6 +165,12 @@ export const useGetFileByPath = (filePath: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetFileByPathQuery = (filePath: string) => {
|
||||
return useQuery<SendRequestResponse<string>, RequestError>(['get-file', filePath], () =>
|
||||
sendRequest<string>({ path: epmRouteService.getFilePath(filePath), method: 'get' })
|
||||
);
|
||||
};
|
||||
|
||||
export const sendGetFileByPath = (filePath: string) => {
|
||||
return sendRequest<string>({
|
||||
path: epmRouteService.getFilePath(filePath),
|
||||
|
@ -184,6 +207,23 @@ export const sendRemovePackage = (pkgName: string, pkgVersion: string, force: bo
|
|||
});
|
||||
};
|
||||
|
||||
interface UpdatePackageArgs {
|
||||
pkgName: string;
|
||||
pkgVersion: string;
|
||||
body: UpdatePackageRequest['body'];
|
||||
}
|
||||
|
||||
export const useUpdatePackageMutation = () => {
|
||||
return useMutation<UpdatePackageResponse, RequestError, UpdatePackageArgs>(
|
||||
({ pkgName, pkgVersion, body }: UpdatePackageArgs) =>
|
||||
sendRequestForRq<UpdatePackageResponse>({
|
||||
path: epmRouteService.getUpdatePath(pkgName, pkgVersion),
|
||||
method: 'put',
|
||||
body,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const sendUpdatePackage = (
|
||||
pkgName: string,
|
||||
pkgVersion: string,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { packagePolicyRouteService } from '../../services';
|
||||
import type {
|
||||
CreatePackagePolicyRequest,
|
||||
|
@ -22,7 +24,8 @@ import type {
|
|||
UpgradePackagePolicyResponse,
|
||||
} from '../../../common/types/rest_spec';
|
||||
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
import type { RequestError } from './use_request';
|
||||
import { sendRequest, sendRequestForRq, useRequest } from './use_request';
|
||||
|
||||
export const sendCreatePackagePolicy = (body: CreatePackagePolicyRequest['body']) => {
|
||||
return sendRequest<CreatePackagePolicyResponse>({
|
||||
|
@ -51,6 +54,16 @@ export const sendDeletePackagePolicy = (body: DeletePackagePoliciesRequest['body
|
|||
});
|
||||
};
|
||||
|
||||
export function useGetPackagePoliciesQuery(query: GetPackagePoliciesRequest['query']) {
|
||||
return useQuery<GetPackagePoliciesResponse, RequestError>(['packagePolicies'], () =>
|
||||
sendRequestForRq<GetPackagePoliciesResponse>({
|
||||
method: 'get',
|
||||
path: packagePolicyRouteService.getListPath(),
|
||||
query,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function useGetPackagePolicies(query: GetPackagePoliciesRequest['query']) {
|
||||
return useRequest<GetPackagePoliciesResponse>({
|
||||
method: 'get',
|
||||
|
@ -67,6 +80,17 @@ export const sendGetPackagePolicies = (query: GetPackagePoliciesRequest['query']
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetOnePackagePolicyQuery = (packagePolicyId: string) => {
|
||||
return useQuery<GetOnePackagePolicyResponse, RequestError>(
|
||||
['packagePolicy', packagePolicyId],
|
||||
() =>
|
||||
sendRequestForRq<GetOnePackagePolicyResponse>({
|
||||
method: 'get',
|
||||
path: packagePolicyRouteService.getInfoPath(packagePolicyId),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetOnePackagePolicy = (packagePolicyId: string) => {
|
||||
return useRequest<GetOnePackagePolicyResponse>({
|
||||
path: packagePolicyRouteService.getInfoPath(packagePolicyId),
|
||||
|
@ -81,6 +105,31 @@ export const sendGetOnePackagePolicy = (packagePolicyId: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export function useUpgradePackagePolicyDryRunQuery(
|
||||
packagePolicyIds: string[],
|
||||
packageVersion?: string,
|
||||
{ enabled }: Partial<{ enabled: boolean }> = {}
|
||||
) {
|
||||
const body: { packagePolicyIds: string[]; packageVersion?: string } = {
|
||||
packagePolicyIds,
|
||||
};
|
||||
|
||||
if (packageVersion) {
|
||||
body.packageVersion = packageVersion;
|
||||
}
|
||||
|
||||
return useQuery<UpgradePackagePolicyDryRunResponse, RequestError>(
|
||||
['upgradePackagePolicyDryRun', packagePolicyIds, packageVersion],
|
||||
() =>
|
||||
sendRequestForRq<UpgradePackagePolicyDryRunResponse>({
|
||||
path: packagePolicyRouteService.getDryRunPath(),
|
||||
method: 'post',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
{ enabled }
|
||||
);
|
||||
}
|
||||
|
||||
export function sendUpgradePackagePolicyDryRun(
|
||||
packagePolicyIds: string[],
|
||||
packageVersion?: string
|
||||
|
@ -100,6 +149,22 @@ export function sendUpgradePackagePolicyDryRun(
|
|||
});
|
||||
}
|
||||
|
||||
export function useUpgradePackagePoliciesMutation() {
|
||||
return useMutation<
|
||||
UpgradePackagePolicyDryRunResponse,
|
||||
RequestError,
|
||||
{ packagePolicyIds: string[] }
|
||||
>(({ packagePolicyIds }) =>
|
||||
sendRequestForRq<UpgradePackagePolicyDryRunResponse>({
|
||||
path: packagePolicyRouteService.getUpgradePath(),
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
packagePolicyIds,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function sendUpgradePackagePolicy(packagePolicyIds: string[]) {
|
||||
return sendRequest<UpgradePackagePolicyResponse>({
|
||||
path: packagePolicyRouteService.getUpgradePath(),
|
||||
|
|
|
@ -5,10 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { settingsRoutesService } from '../../services';
|
||||
import type { PutSettingsResponse, PutSettingsRequest, GetSettingsResponse } from '../../types';
|
||||
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
import type { RequestError } from './use_request';
|
||||
import { sendRequest, sendRequestForRq, useRequest } from './use_request';
|
||||
|
||||
export function useGetSettingsQuery() {
|
||||
return useQuery<GetSettingsResponse, RequestError>(['settings'], () =>
|
||||
sendRequestForRq<GetSettingsResponse>({
|
||||
method: 'get',
|
||||
path: settingsRoutesService.getInfoPath(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function useGetSettings() {
|
||||
return useRequest<GetSettingsResponse>({
|
||||
|
|
|
@ -42,6 +42,26 @@ export const sendRequest = <D = any, E = RequestError>(
|
|||
return _sendRequest<D, E>(httpClient, config);
|
||||
};
|
||||
|
||||
// Sends requests with better ergonomics for React Query, e.g. throw error rather
|
||||
// than resolving with an `error` property in the result. Also returns `data` directly
|
||||
// as opposed to { data } in a response object.
|
||||
export const sendRequestForRq = async <D = any, E = RequestError>(
|
||||
config: SendRequestConfig
|
||||
): Promise<D> => {
|
||||
if (!httpClient) {
|
||||
throw new Error('sendRequest has no http client set');
|
||||
}
|
||||
|
||||
const response = await _sendRequest<D, E>(httpClient, config);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
// Data can't be null so long as `_sendRequest` did not throw
|
||||
return response.data!;
|
||||
};
|
||||
|
||||
export const useRequest = <D = any, E = RequestError>(config: UseRequestConfig) => {
|
||||
if (!httpClient) {
|
||||
throw new Error('sendRequest has no http client set');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue