[8.12] [Fleet] Fix displaying prerelease labels correctly in the installed integration tab (#173319) (#174760)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[Fleet] Fix displaying prerelease labels correctly in the installed
integration tab
(#173319)](https://github.com/elastic/kibana/pull/173319)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nicolas
Chaulet","email":"nicolas.chaulet@elastic.co"},"sourceCommit":{"committedDate":"2023-12-14T15:44:20Z","message":"[Fleet]
Fix displaying prerelease labels correctly in the installed integration
tab
(#173319)","sha":"a0e0c4a860bcc875192aa9c09cc732268d2b8c36","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:skip","Team:Fleet","v8.13.0"],"number":173319,"url":"https://github.com/elastic/kibana/pull/173319","mergeCommit":{"message":"[Fleet]
Fix displaying prerelease labels correctly in the installed integration
tab
(#173319)","sha":"a0e0c4a860bcc875192aa9c09cc732268d2b8c36"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173319","number":173319,"mergeCommit":{"message":"[Fleet]
Fix displaying prerelease labels correctly in the installed integration
tab (#173319)","sha":"a0e0c4a860bcc875192aa9c09cc732268d2b8c36"}}]}]
BACKPORT-->
This commit is contained in:
Nicolas Chaulet 2024-01-15 04:28:34 -05:00 committed by GitHub
parent 68739f97f7
commit d1d01cf2d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 87 deletions

View file

@ -36,7 +36,7 @@ export const IntegrationPreference = () => {
<Component
initialType="recommended"
onChange={action('onChange')}
onPrereleaseEnabledChange={() => {}}
prereleaseIntegrationsEnabled={false}
/>
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
@ -23,7 +23,7 @@ import {
EuiSwitch,
} from '@elastic/eui';
import { sendPutSettings, useGetSettings, useStartServices } from '../../../hooks';
import { usePutSettingsMutation, useStartServices } from '../../../hooks';
export type IntegrationPreferenceType = 'recommended' | 'beats' | 'agent';
@ -35,7 +35,7 @@ interface Option {
export interface Props {
initialType: IntegrationPreferenceType;
onChange: (type: IntegrationPreferenceType) => void;
onPrereleaseEnabledChange: (prerelease: boolean) => void;
prereleaseIntegrationsEnabled: boolean;
}
const recommendedTooltip = (
@ -86,42 +86,39 @@ const options: Option[] = [
export const IntegrationPreference = ({
initialType,
onChange,
onPrereleaseEnabledChange,
prereleaseIntegrationsEnabled,
}: Props) => {
const [idSelected, setIdSelected] = React.useState<IntegrationPreferenceType>(initialType);
const { docLinks } = useStartServices();
const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState<
const [prereleaseIntegrationsChecked, setPrereleaseIntegrationsChecked] = React.useState<
boolean | undefined
>(undefined);
const { data: settings, error: settingsError } = useGetSettings();
const { docLinks, notifications } = useStartServices();
useEffect(() => {
const isEnabled = Boolean(settings?.item.prerelease_integrations_enabled);
if (settings?.item) {
setPrereleaseIntegrationsEnabled(isEnabled);
} else if (settingsError) {
setPrereleaseIntegrationsEnabled(false);
}
}, [settings?.item, settingsError]);
const { mutateAsync: mutateSettingsAsync } = usePutSettingsMutation();
useEffect(() => {
if (prereleaseIntegrationsEnabled !== undefined) {
onPrereleaseEnabledChange(prereleaseIntegrationsEnabled);
}
}, [onPrereleaseEnabledChange, prereleaseIntegrationsEnabled]);
const updateSettings = useCallback(
async (prerelease: boolean) => {
try {
setPrereleaseIntegrationsChecked(prerelease);
const res = await mutateSettingsAsync({
prerelease_integrations_enabled: prerelease,
});
const updateSettings = useCallback(async (prerelease: boolean) => {
const res = await sendPutSettings({
prerelease_integrations_enabled: prerelease,
});
if (res.error) {
throw res.error;
}
}, []);
if (res.error) {
throw res.error;
}
} catch (error) {
setPrereleaseIntegrationsChecked(!prerelease);
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.errorUpdatingSettings', {
defaultMessage: 'Error updating settings',
}),
});
}
},
[mutateSettingsAsync, notifications.toasts]
);
const link = (
<EuiLink href={docLinks.links.fleet.beatsAgentComparison}>
@ -153,16 +150,18 @@ export const IntegrationPreference = ({
EventTarget & { checked: boolean }
>
) => {
const isChecked = event.target.checked;
setPrereleaseIntegrationsEnabled(isChecked);
updateSettings(isChecked);
updateSettings(event.target.checked);
};
return (
<EuiPanel hasShadow={false} paddingSize="none">
<EuiSwitchNoWrap
label="Display beta integrations"
checked={!!prereleaseIntegrationsEnabled}
checked={
typeof prereleaseIntegrationsChecked !== 'undefined'
? prereleaseIntegrationsChecked
: prereleaseIntegrationsEnabled
}
onChange={onPrereleaseSwitchChange}
/>
<EuiSpacer size="l" />

View file

@ -97,9 +97,9 @@ function OnPremLink() {
);
}
export const AvailablePackages: React.FC<{
setPrereleaseEnabled: (isEnabled: boolean) => void;
}> = ({ setPrereleaseEnabled }) => {
export const AvailablePackages: React.FC<{ prereleaseIntegrationsEnabled: boolean }> = ({
prereleaseIntegrationsEnabled,
}) => {
useBreadcrumbs('integrations_all');
const {
@ -121,11 +121,10 @@ export const AvailablePackages: React.FC<{
setUrlandPushHistory,
setUrlandReplaceHistory,
filteredCards,
setPrereleaseIntegrationsEnabled,
availableSubCategories,
selectedSubCategory,
setSelectedSubCategory,
} = useAvailablePackages();
} = useAvailablePackages({ prereleaseIntegrationsEnabled });
const onCategoryChange = useCallback(
({ id }: { id: string }) => {
@ -137,14 +136,6 @@ export const AvailablePackages: React.FC<{
[setCategory, setSearchTerm, setSelectedSubCategory, setUrlandPushHistory]
);
const onPrereleaseEnabledChange = useCallback(
(isEnabled: boolean) => {
setPrereleaseIntegrationsEnabled(isEnabled);
setPrereleaseEnabled(isEnabled);
},
[setPrereleaseIntegrationsEnabled, setPrereleaseEnabled]
);
if (!isLoading && !categoryExists(initialSelectedCategory, allCategories)) {
setUrlandReplaceHistory({ searchString: searchTerm, categoryId: '', subCategoryId: '' });
return null;
@ -155,8 +146,8 @@ export const AvailablePackages: React.FC<{
<EuiHorizontalRule margin="m" />
<IntegrationPreference
initialType={preference}
prereleaseIntegrationsEnabled={prereleaseIntegrationsEnabled}
onChange={setPreference}
onPrereleaseEnabledChange={onPrereleaseEnabledChange}
/>
</EuiFlexItem>,
];

View file

@ -9,8 +9,9 @@ import React from 'react';
import { createIntegrationsTestRendererMock } from '../../../../../../mock';
import type { PackageListItem } from '../../../../types';
import { ExperimentalFeaturesService } from '../../../../services';
import { getIntegrationLabels } from './card_utils';
import { getIntegrationLabels, mapToCard } from './card_utils';
function renderIntegrationLabels(item: Partial<PackageListItem>) {
const renderer = createIntegrationsTestRendererMock();
@ -18,7 +19,73 @@ function renderIntegrationLabels(item: Partial<PackageListItem>) {
return renderer.render(<>{getIntegrationLabels(item as any)}</>);
}
const addBasePath = (s: string) => s;
const getHref = (k: string) => k;
describe('Card utils', () => {
describe('mapToCard', () => {
beforeEach(() => {
ExperimentalFeaturesService.init({});
});
it('should use the installed version if available, without prelease', () => {
const cardItem = mapToCard({
item: {
id: 'test',
version: '2.0.0-preview-1',
installationInfo: {
version: '1.0.0',
},
},
addBasePath,
getHref,
} as any);
expect(cardItem).toMatchObject({
release: 'ga',
version: '1.0.0',
isUpdateAvailable: true,
extraLabelsBadges: undefined,
});
});
it('should use the installed version if available, with prelease ', () => {
const cardItem = mapToCard({
item: {
id: 'test',
version: '2.0.0',
installationInfo: {
version: '1.0.0-preview-1',
},
},
addBasePath,
getHref,
} as any);
expect(cardItem).toMatchObject({
release: 'preview',
version: '1.0.0-preview-1',
isUpdateAvailable: true,
});
});
it('should use the registry version if no installation is available ', () => {
const cardItem = mapToCard({
item: {
id: 'test',
version: '2.0.0-preview-1',
},
addBasePath,
getHref,
} as any);
expect(cardItem).toMatchObject({
release: 'preview',
version: '2.0.0-preview-1',
isUpdateAvailable: false,
});
});
});
describe('getIntegrationLabels', () => {
it('should return an empty list for an integration without errors', () => {
const res = renderIntegrationLabels({

View file

@ -75,7 +75,7 @@ export const mapToCard = ({
let isUnverified = false;
const version = 'version' in item ? item.version || '' : '';
let version = 'version' in item ? item.version || '' : '';
let isUpdateAvailable = false;
let isReauthorizationRequired = false;
@ -84,9 +84,8 @@ export const mapToCard = ({
? addBasePath(item.uiInternalPath)
: item.uiExternalLink || getAbsolutePath(item.uiInternalPath);
} else {
let urlVersion = item.version;
if (item?.installationInfo?.version) {
urlVersion = item.installationInfo.version || item.version;
version = item.installationInfo.version || item.version;
isUnverified = isPackageUnverified(item, packageVerificationKeyId);
isUpdateAvailable = isPackageUpdatable(item);
@ -94,7 +93,7 @@ export const mapToCard = ({
}
const url = getHref('integration_details_overview', {
pkgkey: `${item.name}-${urlVersion}`,
pkgkey: `${item.name}-${version}`,
...(item.integration ? { integration: item.integration } : {}),
});

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import { useState, useMemo } from 'react';
import { uniq } from 'lodash';
@ -103,11 +103,13 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => {
}, []);
};
export const useAvailablePackages = () => {
export const useAvailablePackages = ({
prereleaseIntegrationsEnabled,
}: {
prereleaseIntegrationsEnabled: boolean;
}) => {
const [preference, setPreference] = useState<IntegrationPreferenceType>('recommended');
const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState<
boolean | undefined
>(undefined);
const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get();
const {
@ -245,6 +247,5 @@ export const useAvailablePackages = () => {
eprPackageLoadingError,
eprCategoryLoadingError,
filteredCards,
setPrereleaseIntegrationsEnabled,
};
};

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
import { EuiLoadingSpinner } from '@elastic/eui';
import { installationStatuses } from '../../../../../../../common/constants';
@ -14,7 +15,7 @@ import { INTEGRATIONS_ROUTING_PATHS, INTEGRATIONS_SEARCH_QUERYPARAM } from '../.
import { DefaultLayout } from '../../../../layouts';
import { isPackageUpdatable } from '../../../../services';
import { useGetPackagesQuery } from '../../../../hooks';
import { useAuthz, useGetPackagesQuery, useGetSettingsQuery } from '../../../../hooks';
import type { CategoryFacet, ExtendedIntegrationCategory } from './category_facets';
@ -41,13 +42,24 @@ export const categoryExists = (category: string, categories: CategoryFacet[]) =>
};
export const EPMHomePage: React.FC = () => {
const [prereleaseEnabled, setPrereleaseEnabled] = useState<boolean>(false);
// loading packages to find installed ones
const { data: allPackages, isLoading } = useGetPackagesQuery({
prerelease: prereleaseEnabled,
const authz = useAuthz();
const isAuthorizedToFetchSettings = authz.fleet.all;
const { data: settings, isFetchedAfterMount: isSettingsFetched } = useGetSettingsQuery({
enabled: isAuthorizedToFetchSettings,
});
const prereleaseIntegrationsEnabled = settings?.item.prerelease_integrations_enabled ?? false;
const shouldFetchPackages = !isAuthorizedToFetchSettings || isSettingsFetched;
// loading packages to find installed ones
const { data: allPackages, isLoading } = useGetPackagesQuery(
{
prerelease: prereleaseIntegrationsEnabled,
},
{
enabled: shouldFetchPackages,
}
);
const installedPackages = useMemo(
() =>
(allPackages?.items || []).filter(
@ -76,6 +88,11 @@ export const EPMHomePage: React.FC = () => {
const notificationsBySection = {
manage: unverifiedPackageCount + upgradeablePackageCount,
};
if (!shouldFetchPackages) {
return <EuiLoadingSpinner />;
}
return (
<Routes>
<Route path={INTEGRATIONS_ROUTING_PATHS.integrations_installed}>
@ -85,7 +102,7 @@ export const EPMHomePage: React.FC = () => {
</Route>
<Route path={INTEGRATIONS_ROUTING_PATHS.integrations_all}>
<DefaultLayout section="browse" notificationsBySection={notificationsBySection}>
<AvailablePackages setPrereleaseEnabled={setPrereleaseEnabled} />
<AvailablePackages prereleaseIntegrationsEnabled={prereleaseIntegrationsEnabled} />
</DefaultLayout>
</Route>
</Routes>

View file

@ -82,15 +82,21 @@ 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',
version: API_VERSIONS.public.v1,
query,
})
);
export const useGetPackagesQuery = (
query: GetPackagesRequest['query'],
options?: { enabled?: boolean }
) => {
return useQuery<GetPackagesResponse, RequestError>({
queryKey: ['get-packages', query],
queryFn: () =>
sendRequestForRq<GetPackagesResponse>({
path: epmRouteService.getListPath(),
method: 'get',
version: API_VERSIONS.public.v1,
query,
}),
enabled: options?.enabled,
});
};
export const sendGetPackages = (query: GetPackagesRequest['query'] = {}) => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { settingsRoutesService } from '../../services';
import type { PutSettingsResponse, PutSettingsRequest, GetSettingsResponse } from '../../types';
@ -15,14 +15,17 @@ import { API_VERSIONS } from '../../../common/constants';
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(),
version: API_VERSIONS.public.v1,
})
);
export function useGetSettingsQuery(options?: { enabled?: boolean }) {
return useQuery<GetSettingsResponse, RequestError>({
queryKey: ['settings'],
enabled: options?.enabled,
queryFn: () =>
sendRequestForRq<GetSettingsResponse>({
method: 'get',
path: settingsRoutesService.getInfoPath(),
version: API_VERSIONS.public.v1,
}),
});
}
export function useGetSettings() {
@ -41,6 +44,17 @@ export function sendGetSettings() {
});
}
export function usePutSettingsMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: sendPutSettings,
onSuccess: () => {
queryClient.invalidateQueries(['settings']);
},
});
}
export function sendPutSettings(body: PutSettingsRequest['body']) {
return sendRequest<PutSettingsResponse>({
method: 'put',