From eba8e454f5af8695196abaa71745f2657ca211f0 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 18 Oct 2019 10:32:41 -0400 Subject: [PATCH] [IM] add screenshot detail view and image endpoint (#48149) * add api endpoint to handle images * #47978 add screenshots component with single image * update padding around screenshot * import existing API_ROOT * move ImageRequestParams interface and fix type * pass the content-type through and add test * fix ie11 issues with nested flex items, change radius to use eui variable * use eui variables for padding * add aria label and image description --- .../integrations_manager/common/types.ts | 5 ++ .../public/hooks/use_links.tsx | 2 + .../public/screens/detail/content.tsx | 8 ++- .../public/screens/detail/overview_panel.tsx | 14 ++--- .../public/screens/detail/screenshots.tsx | 57 +++++++++++++++++ .../server/integrations/get.ts | 3 + .../server/integrations/handlers.ts | 14 +++++ .../server/registry/index.ts | 14 ++++- .../integrations_manager/server/routes.ts | 8 +++ .../apis/image.ts | 61 +++++++++++++++++++ .../apis/index.js | 1 + 11 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 x-pack/legacy/plugins/integrations_manager/public/screens/detail/screenshots.tsx create mode 100644 x-pack/test/integrations_manager_api_integration/apis/image.ts diff --git a/x-pack/legacy/plugins/integrations_manager/common/types.ts b/x-pack/legacy/plugins/integrations_manager/common/types.ts index 1e63509e91dc4..3a3f1330a8b78 100644 --- a/x-pack/legacy/plugins/integrations_manager/common/types.ts +++ b/x-pack/legacy/plugins/integrations_manager/common/types.ts @@ -32,6 +32,10 @@ export interface RegistryListItem { title?: string; } +export interface ScreenshotItem { + src: string; +} + // from /package/{name} // https://github.com/elastic/integrations-registry/blob/master/docs/api/package.json export type ServiceName = 'kibana' | 'elasticsearch' | 'filebeat' | 'metricbeat'; @@ -67,6 +71,7 @@ export interface RegistryPackage { icon: string; requirement: RequirementsByServiceName; title?: string; + screenshots?: ScreenshotItem[]; } // Managers public HTTP response types diff --git a/x-pack/legacy/plugins/integrations_manager/public/hooks/use_links.tsx b/x-pack/legacy/plugins/integrations_manager/public/hooks/use_links.tsx index 3c5cfbe75c5f2..786ec54b9c81c 100644 --- a/x-pack/legacy/plugins/integrations_manager/public/hooks/use_links.tsx +++ b/x-pack/legacy/plugins/integrations_manager/public/hooks/use_links.tsx @@ -6,6 +6,7 @@ import { generatePath } from 'react-router-dom'; import { PLUGIN } from '../../common/constants'; +import { API_ROOT } from '../../common/routes'; import { patterns } from '../routes'; import { useCore } from '.'; import { DetailViewPanelName } from '..'; @@ -31,6 +32,7 @@ function appRoot(path: string) { export function useLinks() { return { toAssets: (path: string) => addBasePath(`/plugins/${PLUGIN.ID}/assets/${path}`), + toImage: (path: string) => addBasePath(`${API_ROOT}${path}`), toListView: () => appRoot(patterns.LIST_VIEW), toDetailView: ({ name, version, panel }: DetailParams) => { // panel is optional, but `generatePath` won't accept `path: undefined` diff --git a/x-pack/legacy/plugins/integrations_manager/public/screens/detail/content.tsx b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/content.tsx index 92f7b82b1bdc3..795f82a37ce64 100644 --- a/x-pack/legacy/plugins/integrations_manager/public/screens/detail/content.tsx +++ b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/content.tsx @@ -37,8 +37,12 @@ export function Content(props: ContentProps) { ` : LeftColumn; + // fixes IE11 problem with nested flex items + const ContentFlexGroup = styled(EuiFlexGroup)` + flex: 0 0 auto !important; + `; return ( - + @@ -48,7 +52,7 @@ export function Content(props: ContentProps) { - + ); } diff --git a/x-pack/legacy/plugins/integrations_manager/public/screens/detail/overview_panel.tsx b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/overview_panel.tsx index 01c4a0d4458b7..d2765f8845d35 100644 --- a/x-pack/legacy/plugins/integrations_manager/public/screens/detail/overview_panel.tsx +++ b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/overview_panel.tsx @@ -6,25 +6,21 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { IntegrationInfo } from '../../../common/types'; +import { Screenshots } from './screenshots'; export function OverviewPanel(props: IntegrationInfo) { - const { description } = props; + const { description, screenshots } = props; return ( - - About + +

About

{description}

Still need a) longer descriptions b) component to show/hide

- - Screenshots - - -

Where are we getting these images?

-
+ {screenshots && }
); } diff --git a/x-pack/legacy/plugins/integrations_manager/public/screens/detail/screenshots.tsx b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/screenshots.tsx new file mode 100644 index 0000000000000..f20597ab13bf0 --- /dev/null +++ b/x-pack/legacy/plugins/integrations_manager/public/screens/detail/screenshots.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiText, EuiTitle, EuiImage, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { ScreenshotItem } from '../../../common/types'; +import { useLinks, useCore } from '../../hooks'; + +interface ScreenshotProps { + images: ScreenshotItem[]; +} + +export function Screenshots(props: ScreenshotProps) { + const { theme } = useCore(); + const { toImage } = useLinks(); + const { images } = props; + + // for now, just get first image + const src = toImage(images[0].src); + + const horizontalPadding: number = parseInt(theme.eui.paddingSizes.xl, 10) * 2; + const bottomPadding: number = parseInt(theme.eui.paddingSizes.xl, 10) * 1.75; + + const ScreenshotsContainer = styled(EuiFlexGroup)` + background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%), + ${theme.eui.euiColorPrimary}; + padding: ${theme.eui.paddingSizes.xl} ${horizontalPadding}px ${bottomPadding}px; + flex: 0 0 auto; + border-radius: ${theme.eui.euiBorderRadius}; + `; + // fixes ie11 problems with nested flex items + const NestedEuiFlexItem = styled(EuiFlexItem)` + flex: 0 0 auto !important; + `; + return ( + + +

Screenshots

+
+ + + + + We need image descriptions to be returned in the response + + + + + + + +
+ ); +} diff --git a/x-pack/legacy/plugins/integrations_manager/server/integrations/get.ts b/x-pack/legacy/plugins/integrations_manager/server/integrations/get.ts index b74c037bde86c..c10bb32855613 100644 --- a/x-pack/legacy/plugins/integrations_manager/server/integrations/get.ts +++ b/x-pack/legacy/plugins/integrations_manager/server/integrations/get.ts @@ -68,6 +68,9 @@ export async function getIntegrationInfo(options: { return createInstallableFrom(updated, savedObject); } +export const getImage = async (options: Registry.ImageRequestParams) => + Registry.fetchImage(options); + export async function getInstallationObject(options: { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; diff --git a/x-pack/legacy/plugins/integrations_manager/server/integrations/handlers.ts b/x-pack/legacy/plugins/integrations_manager/server/integrations/handlers.ts index cbb5b4cd90cac..0cd8ef406c802 100644 --- a/x-pack/legacy/plugins/integrations_manager/server/integrations/handlers.ts +++ b/x-pack/legacy/plugins/integrations_manager/server/integrations/handlers.ts @@ -12,10 +12,12 @@ import { getCategories, getClusterAccessor, getIntegrationInfo, + getImage, getIntegrations, installIntegration, removeInstallation, } from './index'; +import { ImageRequestParams } from '../registry'; interface Extra extends ResponseToolkit { context: PluginContext; @@ -31,6 +33,10 @@ interface PackageRequest extends Request { }; } +interface ImageRequest extends Request { + params: Request['params'] & ImageRequestParams; +} + interface InstallAssetRequest extends Request { params: AssetRequestParams; } @@ -65,6 +71,14 @@ export async function handleGetInfo(req: PackageRequest, extra: Extra) { return integrationInfo; } +export const handleGetImage = async (req: ImageRequest, extra: Extra) => { + const response = await getImage(req.params); + const newResponse = extra.response(response.body); + // set the content type from the registry response + newResponse.header('Content-Type', response.headers.get('content-type') || ''); + return newResponse; +}; + export async function handleRequestInstall(req: InstallAssetRequest, extra: Extra) { const { pkgkey, asset } = req.params; if (!asset) throw new Error('Unhandled empty/default asset case'); diff --git a/x-pack/legacy/plugins/integrations_manager/server/registry/index.ts b/x-pack/legacy/plugins/integrations_manager/server/registry/index.ts index 3154d03ff33c2..ed005c34f27b9 100644 --- a/x-pack/legacy/plugins/integrations_manager/server/registry/index.ts +++ b/x-pack/legacy/plugins/integrations_manager/server/registry/index.ts @@ -5,6 +5,7 @@ */ import { URL } from 'url'; +import { Response } from 'node-fetch'; import { AssetsGroupedByServiceByType, AssetParts, @@ -15,7 +16,7 @@ import { } from '../../common/types'; import { cacheGet, cacheSet } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; -import { fetchUrl, getResponseStream } from './requests'; +import { fetchUrl, getResponseStream, getResponse } from './requests'; import { streamToBuffer } from './streams'; import { integrationsManagerConfigStore } from '../config'; @@ -25,6 +26,11 @@ export interface SearchParams { category?: CategoryId; } +export interface ImageRequestParams { + pkgkey: string; + imgPath: string; +} + export async function fetchList(params?: SearchParams): Promise { const { registryUrl } = integrationsManagerConfigStore.getConfig(); const url = new URL(`${registryUrl}/search`); @@ -40,6 +46,12 @@ export async function fetchInfo(key: string): Promise { return fetchUrl(`${registryUrl}/package/${key}`).then(JSON.parse); } +export async function fetchImage(params: ImageRequestParams): Promise { + const { registryUrl } = integrationsManagerConfigStore.getConfig(); + const { pkgkey, imgPath } = params; + return getResponse(`${registryUrl}/package/${pkgkey}/img/${imgPath}`); +} + export async function fetchCategories(): Promise { const { registryUrl } = integrationsManagerConfigStore.getConfig(); return fetchUrl(`${registryUrl}/categories`).then(JSON.parse); diff --git a/x-pack/legacy/plugins/integrations_manager/server/routes.ts b/x-pack/legacy/plugins/integrations_manager/server/routes.ts index 8240ff152b156..a25bc0e21ce03 100644 --- a/x-pack/legacy/plugins/integrations_manager/server/routes.ts +++ b/x-pack/legacy/plugins/integrations_manager/server/routes.ts @@ -8,6 +8,8 @@ import { ServerRoute } from '../common/types'; import * as CommonRoutes from '../common/routes'; import * as Integrations from './integrations/handlers'; +const API_IMG_PATTERN = `${CommonRoutes.API_ROOT}/package/{pkgkey}/img/{imgPath*}`; + // Manager public API paths export const routes: ServerRoute[] = [ { @@ -22,6 +24,12 @@ export const routes: ServerRoute[] = [ options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } }, handler: Integrations.handleGetList, }, + { + method: 'GET', + path: API_IMG_PATTERN, + options: { tags: [`access:${PLUGIN.ID}`], json: { space: 2 } }, + handler: Integrations.handleGetImage, + }, { method: 'GET', path: CommonRoutes.API_INFO_PATTERN, diff --git a/x-pack/test/integrations_manager_api_integration/apis/image.ts b/x-pack/test/integrations_manager_api_integration/apis/image.ts new file mode 100644 index 0000000000000..80358dbf2a129 --- /dev/null +++ b/x-pack/test/integrations_manager_api_integration/apis/image.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ServerMock from 'mock-http-server'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + describe('images', () => { + const server = new ServerMock({ host: 'localhost', port: 6666 }); + beforeEach(() => { + server.start(() => {}); + }); + afterEach(() => { + server.stop(() => {}); + }); + it('fetches a png screenshot image from the registry', async () => { + server.on({ + method: 'GET', + path: '/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png', + reply: { + headers: { 'content-type': 'image/png' }, + }, + }); + + const supertest = getService('supertest'); + const fetchImage = async () => { + await supertest + .get( + '/api/integrations_manager/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png' + ) + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/png') + .expect(200); + }; + await fetchImage(); + }); + + it('fetches an icon image from the registry', async () => { + server.on({ + method: 'GET', + path: '/package/auditd-2.0.4/img/icon.svg', + reply: { + headers: { 'content-type': 'image/svg' }, + }, + }); + + const supertest = getService('supertest'); + const fetchImage = async () => { + await supertest + .get('/api/integrations_manager/package/auditd-2.0.4/img/icon.svg') + .set('kbn-xsrf', 'xxx') + .expect('Content-Type', 'image/svg') + .expect(200); + }; + await fetchImage(); + }); + }); +} diff --git a/x-pack/test/integrations_manager_api_integration/apis/index.js b/x-pack/test/integrations_manager_api_integration/apis/index.js index 051e2f807ed44..7f8c8bbad503c 100644 --- a/x-pack/test/integrations_manager_api_integration/apis/index.js +++ b/x-pack/test/integrations_manager_api_integration/apis/index.js @@ -8,5 +8,6 @@ export default function ({ loadTestFile }) { describe('Integrations Manager Endpoints', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./list')); + loadTestFile(require.resolve('./image')); }); }