Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rough prototype UI demo for asset CRUD #2

Draft
wants to merge 3 commits into
base: 40044-kibana-asset
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions x-pack/legacy/plugins/integrations_manager/common/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ export function getListPath() {
export function getInfoPath(pkgkey: string) {
return API_INFO_PATTERN.replace('{pkgkey}', pkgkey);
}

export function getInstallPath(pkgkey: string, feature: string = '') {
return API_INSTALL_PATTERN.replace('{pkgkey}', pkgkey)
.replace('{feature?}', feature)
.replace(/\/$/, ''); // trim trailing slash
}

export function getRemovePath(pkgkey: string, feature: string = '') {
return API_DELETE_PATTERN.replace('{pkgkey}', pkgkey)
.replace('{feature?}', feature)
.replace(/\/$/, ''); // trim trailing slash
}
33 changes: 32 additions & 1 deletion x-pack/legacy/plugins/integrations_manager/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedObject } from '../../../../../target/types/server';
import { SavedObjectAttributes } from '../../../../../src/core/server';

export { Request, ServerRoute } from 'hapi';

// the contract with the registry
export type IntegrationList = IntegrationListItem[];
export type IntegrationStatus = 'installed' | 'not_installed';
export type IntegrationAssetType =
| 'ingest-pipeline'
| 'visualization'
| 'dashboard'
| 'index-pattern';

export interface IntegrationAsset {
href: string;
id: string;
type: IntegrationAssetType;
description?: string;
title?: string;
}

export interface IntegrationStateSavedObject extends SavedObjectAttributes {
installed: IntegrationAsset[];
}

// registry /list
// https://github.com/elastic/integrations-registry/blob/master/docs/api/list.json
Expand All @@ -17,19 +38,29 @@ export interface IntegrationListItem {
icon: string;
name: string;
version: string;
status: IntegrationStatus;
savedObject?: SavedObject;
}

// registry /package/{name}
// https://github.com/elastic/integrations-registry/blob/master/docs/api/package.json
export interface IntegrationInfo {
export interface IntegrationProps {
name: string;
version: string;
description: string;
icon: string;
status: IntegrationStatus;
availableAssets: IntegrationAssetType[];
installedAssets: IntegrationAsset[];
requirement: {
kibana: {
min: string;
max: string;
};
};
}

// lifted from APM
export type PromiseReturnType<Func> = Func extends (...args: any[]) => Promise<infer Value>
? Value
: Func;
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { IntegrationList, IntegrationListItem } from '../../common/types';
import { EuiFlexGrid, EuiFlexItem, EuiTitle, EuiSpacer, EuiText } from '@elastic/eui';
import { IntegrationListItem, PromiseReturnType, IntegrationList } from '../../common/types';
import { IntegrationCard } from './integration_card';
import { getIntegrationsGroupedByState } from '../data';

interface ListProps {
list: IntegrationList;
list: PromiseReturnType<typeof getIntegrationsGroupedByState>;
}

export function IntegrationListGrid({ list }: ListProps) {
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.map(item => (
<GridItem key={`${item.name}-${item.version}`} {...item} />
))}
</EuiFlexGrid>
<div>
<EuiTitle>
<h3>Your integrations</h3>
</EuiTitle>

<GridGroup list={list.installed} />

<EuiSpacer size="xl" />

<EuiTitle>
<h3>Available integrations</h3>
</EuiTitle>
<EuiSpacer size="m" />
<GridGroup list={list.not_installed} />
</div>
);
}

Expand All @@ -29,3 +40,20 @@ function GridItem(item: IntegrationListItem) {
</EuiFlexItem>
);
}

function GridGroup({ list = [] }: { list?: IntegrationList }) {
if (!list || list.length === 0) {
return (
<EuiText>
<p>No integrations found.</p>
</EuiText>
);
}
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.map(item => (
<GridItem key={`${item.name}-${item.version}`} {...item} />
))}
</EuiFlexGrid>
);
}
50 changes: 46 additions & 4 deletions x-pack/legacy/plugins/integrations_manager/public/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,27 @@

import { npStart } from 'ui/new_platform';
import { HttpHandler } from 'src/core/public/http';
import { IntegrationInfo, IntegrationList } from '../common/types';
import { getListPath, getInfoPath } from '../common/routes';
import { Reducer } from 'react';
import { IntegrationProps, IntegrationList } from '../common/types';
import { getListPath, getInfoPath, getInstallPath, getRemovePath } from '../common/routes';

let _fetch: HttpHandler = npStart.core.http.fetch;

export interface AsyncFetchState {
loading: boolean;
success: boolean;
failure: boolean;
error?: Error | null;
}

export const asyncFetchReducer: Reducer<AsyncFetchState, Partial<AsyncFetchState>> = (
state,
newState
) => ({
...state,
...newState,
});

export function setClient(client: HttpHandler): void {
_fetch = client;
}
Expand All @@ -22,9 +38,35 @@ export async function getIntegrationsList(): Promise<IntegrationList> {
return list;
}

export async function getIntegrationInfoByKey(pkgkey: string): Promise<IntegrationInfo> {
export async function getIntegrationsGroupedByState() {
const path = getListPath();
const list: IntegrationList = await _fetch(path);

return list.reduce(
(grouped: { not_installed: IntegrationList; installed: IntegrationList }, item) => {
if (!grouped[item.status]) {
grouped[item.status] = [];
}
grouped[item.status].push(item);
return grouped;
},
{ installed: [], not_installed: [] }
);
}

export async function getIntegrationInfoByKey(pkgkey: string): Promise<IntegrationProps> {
const path = getInfoPath(pkgkey);
const info: IntegrationInfo = await _fetch(path);
const info: IntegrationProps = await _fetch(path);

return info;
}

export async function installIntegration(pkgkey: string) {
const path = getInstallPath(pkgkey);
return await _fetch(path);
}

export async function removeIntegration(pkgkey: string) {
const path = getRemovePath(pkgkey);
return await _fetch(path);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.
*/

/*
* 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, { useState, useEffect } from 'react';
import { getIntegrationInfoByKey } from '../../data';
import { IntegrationProps } from '../../../common/types';
import { InstalledIntegration } from './installed';
import { NotInstalledIntegration } from './not_installed';

export const AssetTitleMap = {
'ingest-pipeline': 'Ingest Pipeline',
dashboard: 'Dashboard',
visualization: 'Visualization',
'index-pattern': 'Index Pattern',
};

export function Detail(props: { package: string }) {
const [info, setInfo] = useState<IntegrationProps | null>(null);
useEffect(loadIntegration, [props.package]);

function loadIntegration() {
getIntegrationInfoByKey(props.package).then(setInfo);
}

// don't have designs for loading/empty states
if (!info) return 'Loading integration detail page...';

return info.status === 'installed' ? (
<InstalledIntegration {...info} reload={loadIntegration} />
) : (
<NotInstalledIntegration {...info} reload={loadIntegration} />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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, { useReducer } from 'react';
import { EuiText, EuiIcon, EuiButton, EuiSpacer, EuiLink, EuiPanel, EuiTitle } from '@elastic/eui';
import { IntegrationProps } from '../../../common/types';
import { removeIntegration, asyncFetchReducer } from '../../data';

export function InstalledIntegration(props: IntegrationProps & { reload: () => void }) {
const { description, name, version, installedAssets, reload } = props;
const [removalState, dispatch] = useReducer(asyncFetchReducer, {
loading: false,
success: false,
failure: false,
});

async function remove() {
if (removalState.loading) {
return;
}
dispatch({ loading: true });
try {
await removeIntegration(`${name}-${version}`);
} catch (error) {
dispatch({ loading: false, failure: true, error });
return;
}
dispatch({ loading: false, success: true, error: null });
reload();
}

function RemoveButton() {
if (removalState.failure) {
// eslint-disable-next-line no-console
console.log('An error occurred during removal', removalState.error);
return (
<EuiText>
<p>There was a problem removing this integration.</p>
</EuiText>
);
}
return (
<div>
<EuiButton disabled={removalState.loading} onClick={remove}>
Remove this integration
</EuiButton>
<EuiSpacer size="s" />
<EuiText size="xs">
<p>This will remove all of the installed assets for this integration.</p>
</EuiText>
</div>
);
}

return (
<EuiPanel>
<EuiLink href={window.location.href.replace(/#.*$/, '')}>
<EuiIcon type="arrowLeft" /> Back to list
</EuiLink>
<EuiSpacer />
<EuiTitle>
<h1>{`${name} (v${version})`}</h1>
</EuiTitle>
<EuiSpacer />
<p>{description}</p>
<EuiSpacer />
<EuiTitle size="s">
<h5>Included in this install:</h5>
</EuiTitle>
<ul style={{ lineHeight: '1.5' }}>
{installedAssets.map(asset => (
<li key={asset.id}>
<EuiLink target="_blank" href={asset.href}>
<EuiIcon type="check" /> {asset.type}:{' '}
{asset.description ? asset.description : asset.title ? asset.title : asset.id}
</EuiLink>
</li>
))}
</ul>
<EuiSpacer />
{removalState.loading ? <p>Removing assets...</p> : null}
<EuiSpacer />
<RemoveButton />
</EuiPanel>
);
}
Loading