Routing example plugin (#69581)

* Routing example plugin

* Routing example plugin

* address review comments

* consolidate route registration into single function

* ts fix

* Add functional tests

* typescript fix

* Fix typo

* check against HttpFetchError not Error

* fix ts

* fix unhappy ci

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Stacey Gammon 2020-07-15 12:44:42 -04:00 committed by GitHub
parent badb7b5c8c
commit 339e13bc53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1107 additions and 0 deletions

View file

@ -0,0 +1,9 @@
Team owner: Platform
A working example of a plugin that registers and uses multiple custom routes.
Read more:
- [IRouter API Docs](../../docs/development/core/server/kibana-plugin-core-server.irouter.md)
- [HttpHandler (core.http.fetch) API Docs](../../docs/development/core/public/kibana-plugin-core-public.httphandler.md)
- [Routing Conventions](../../STYLEGUIDE.md#api-endpoints)

View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const RANDOM_NUMBER_ROUTE_PATH = '/api/random_number';
export const RANDOM_NUMBER_BETWEEN_ROUTE_PATH = '/api/random_number_between';
export const POST_MESSAGE_ROUTE_PATH = '/api/post_message';
// Internal APIs should use the `internal` prefix, instead of the `api` prefix.
export const INTERNAL_GET_MESSAGE_BY_ID_ROUTE = '/internal/get_message';

View file

@ -0,0 +1,9 @@
{
"id": "routingExample",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["developerExamples"],
"optionalPlugins": []
}

View file

@ -0,0 +1,105 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters } from 'kibana/public';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiText,
EuiHorizontalRule,
EuiPageContentHeader,
EuiListGroup,
} from '@elastic/eui';
import { RandomNumberRouteExample } from './random_number_example';
import { RandomNumberBetweenRouteExample } from './random_number_between_example';
import { Services } from './services';
import { PostMessageRouteExample } from './post_message_example';
import { GetMessageRouteExample } from './get_message_example';
type Props = Services;
function RoutingExplorer({
fetchRandomNumber,
fetchRandomNumberBetween,
addSuccessToast,
postMessage,
getMessageById,
}: Props) {
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentHeader>
<EuiText>
<h1>Routing examples</h1>
</EuiText>
</EuiPageContentHeader>
<EuiText>
<EuiListGroup
listItems={[
{
label: 'IRouter API docs',
href:
'https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.irouter.md',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
{
label: 'HttpHandler (core.http.fetch) API docs',
href:
'https://github.com/elastic/kibana/blob/master/docs/development/core/public/kibana-plugin-core-public.httphandler.md',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
{
label: 'Conventions',
href: 'https://github.com/elastic/kibana/tree/master/STYLEGUIDE.md#api-endpoints',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
]}
/>
</EuiText>
<EuiHorizontalRule />
<RandomNumberRouteExample fetchRandomNumber={fetchRandomNumber} />
<EuiHorizontalRule />
<RandomNumberBetweenRouteExample fetchRandomNumberBetween={fetchRandomNumberBetween} />
<EuiHorizontalRule />
<PostMessageRouteExample addSuccessToast={addSuccessToast} postMessage={postMessage} />
<EuiHorizontalRule />
<GetMessageRouteExample getMessageById={getMessageById} />
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
export const renderApp = (props: Props, element: AppMountParameters['element']) => {
ReactDOM.render(<RoutingExplorer {...props} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { useState } from 'react';
import {
EuiText,
EuiButton,
EuiLoadingSpinner,
EuiFieldText,
EuiCallOut,
EuiFormRow,
} from '@elastic/eui';
import { HttpFetchError } from '../../../src/core/public';
import { isError } from './is_error';
import { Services } from './services';
interface Props {
getMessageById: Services['getMessageById'];
}
export function GetMessageRouteExample({ getMessageById }: Props) {
const [error, setError] = useState<HttpFetchError | undefined>();
const [isFetching, setIsFetching] = useState<boolean>(false);
const [message, setMessage] = useState<string>('');
const [id, setId] = useState<string>('');
const doFetch = useCallback(async () => {
if (isFetching) return;
setIsFetching(true);
const response = await getMessageById(id);
if (isError(response)) {
setError(response);
setMessage('');
} else {
setError(undefined);
setMessage(response);
}
setIsFetching(false);
}, [isFetching, getMessageById, setMessage, id]);
return (
<React.Fragment>
<EuiText>
<h2>GET example with param</h2>
<p>This examples uses a simple GET route that takes an id as a param in the route path.</p>
<EuiFormRow label="Message Id">
<EuiFieldText
value={id}
onChange={(e) => setId(e.target.value)}
data-test-subj="routingExampleGetMessageId"
/>
</EuiFormRow>
<EuiFormRow hasEmptyLabelSpace={true}>
<EuiButton
data-test-subj="routingExampleFetchMessage"
disabled={isFetching || id === ''}
onClick={() => doFetch()}
>
{isFetching ? <EuiLoadingSpinner /> : 'Get message'}
</EuiButton>
</EuiFormRow>
{error !== undefined ? (
<EuiCallOut color="danger" iconType="alert">
{error.message}
</EuiCallOut>
) : null}
{message !== '' ? (
<p>
Message is: <pre data-test-subj="routingExampleGetMessage">{message}</pre>
</p>
) : null}
</EuiText>
</React.Fragment>
);
}

View file

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

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { HttpFetchError } from '../../../src/core/public';
export function isError<T>(error: T | HttpFetchError): error is HttpFetchError {
return error instanceof HttpFetchError;
}

View file

@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CoreStart,
Plugin,
CoreSetup,
AppMountParameters,
AppNavLinkStatus,
} from '../../../src/core/public';
import { DeveloperExamplesSetup } from '../../developer_examples/public';
import { getServices } from './services';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
}
export class RoutingExamplePlugin implements Plugin<{}, {}, SetupDeps, {}> {
public setup(core: CoreSetup, { developerExamples }: SetupDeps) {
core.application.register({
id: 'routingExample',
title: 'Routing',
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
const [coreStart] = await core.getStartServices();
const startServices = getServices(coreStart);
const { renderApp } = await import('./app');
return renderApp(startServices, params.element);
},
});
developerExamples.register({
appId: 'routingExample',
title: 'Routing',
description: `Examples show how to use core routing and fetch services to register and query your own custom routes.`,
links: [
{
label: 'IRouter',
href:
'https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.irouter.md',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
{
label: 'HttpHandler (core.http.fetch)',
href:
'https://github.com/elastic/kibana/blob/master/docs/development/core/public/kibana-plugin-core-public.httphandler.md',
iconType: 'logoGithub',
target: '_blank',
size: 's',
},
],
});
return {};
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,103 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { useState } from 'react';
import {
EuiText,
EuiButton,
EuiLoadingSpinner,
EuiFieldText,
EuiCallOut,
EuiFormRow,
EuiTextArea,
} from '@elastic/eui';
import { HttpFetchError } from '../../../src/core/public';
import { isError } from './is_error';
import { Services } from './services';
interface Props {
postMessage: Services['postMessage'];
addSuccessToast: Services['addSuccessToast'];
}
export function PostMessageRouteExample({ postMessage, addSuccessToast }: Props) {
const [error, setError] = useState<HttpFetchError | undefined>();
const [isPosting, setIsPosting] = useState<boolean>(false);
const [message, setMessage] = useState<string>('');
const [id, setId] = useState<string>('');
const doFetch = useCallback(async () => {
if (isPosting) return;
setIsPosting(true);
const response = await postMessage(message, id);
if (response && isError(response)) {
setError(response);
} else {
setError(undefined);
addSuccessToast('Message was added!');
setMessage('');
setId('');
}
setIsPosting(false);
}, [isPosting, postMessage, addSuccessToast, setMessage, message, id]);
return (
<React.Fragment>
<EuiText>
<h2>POST example with body</h2>
<p>
This examples uses a simple POST route that takes a body parameter and an id as a param in
the route path.
</p>
<EuiFormRow label="Message Id">
<EuiFieldText
value={id}
onChange={(e) => setId(e.target.value)}
data-test-subj="routingExampleSetMessageId"
/>
</EuiFormRow>
<EuiFormRow label="Message">
<EuiTextArea
data-test-subj="routingExampleSetMessage"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</EuiFormRow>
<EuiFormRow hasEmptyLabelSpace={true}>
<EuiButton
data-test-subj="routingExamplePostMessage"
disabled={isPosting || id === '' || message === ''}
onClick={() => doFetch()}
>
{isPosting ? <EuiLoadingSpinner /> : 'Post message'}
</EuiButton>
</EuiFormRow>
{error !== undefined ? (
<EuiCallOut color="danger" iconType="alert">
{error.message}
</EuiCallOut>
) : null}
</EuiText>
</React.Fragment>
);
}

View file

@ -0,0 +1,98 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { useState } from 'react';
import {
EuiText,
EuiButton,
EuiLoadingSpinner,
EuiFieldText,
EuiCallOut,
EuiFormRow,
} from '@elastic/eui';
import { HttpFetchError } from '../../../src/core/public';
import { isError } from './is_error';
import { Services } from './services';
interface Props {
fetchRandomNumberBetween: Services['fetchRandomNumberBetween'];
}
export function RandomNumberBetweenRouteExample({ fetchRandomNumberBetween }: Props) {
const [error, setError] = useState<HttpFetchError | undefined>();
const [randomNumber, setRandomNumber] = useState<number>(0);
const [isFetching, setIsFetching] = useState<boolean>(false);
const [maxInput, setMaxInput] = useState<string>('10');
const doFetch = useCallback(async () => {
if (isFetching) return;
setIsFetching(true);
const response = await fetchRandomNumberBetween(Number.parseInt(maxInput, 10));
if (isError(response)) {
setError(response);
} else {
setRandomNumber(response);
}
setIsFetching(false);
}, [isFetching, maxInput, fetchRandomNumberBetween]);
return (
<React.Fragment>
<EuiText>
<h2>GET example with query</h2>
<p>
This examples uses a simple GET route that takes a query parameter in the request and
returns a single number.
</p>
<EuiFormRow label="Generate a random number between 0 and">
<EuiFieldText
data-test-subj="routingExampleMaxRandomNumberBetween"
value={maxInput}
onChange={(e) => setMaxInput(e.target.value)}
isInvalid={isNaN(Number(maxInput))}
/>
</EuiFormRow>
<EuiFormRow hasEmptyLabelSpace={true}>
<EuiButton
data-test-subj="routingExampleFetchRandomNumberBetween"
disabled={isFetching || isNaN(Number(maxInput))}
onClick={() => doFetch()}
>
{isFetching ? <EuiLoadingSpinner /> : 'Generate random number'}
</EuiButton>
</EuiFormRow>
{error !== undefined ? (
<EuiCallOut color="danger" iconType="alert">
{error.message}
</EuiCallOut>
) : null}
{randomNumber > -1 ? (
<h2>
Random number is
<div data-test-subj="routingExampleRandomNumberBetween">{randomNumber}</div>
</h2>
) : null}
</EuiText>
</React.Fragment>
);
}

View file

@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { useState } from 'react';
import { EuiText, EuiButton, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui';
import { HttpFetchError } from '../../../src/core/public';
import { Services } from './services';
import { isError } from './is_error';
interface Props {
fetchRandomNumber: Services['fetchRandomNumber'];
}
export function RandomNumberRouteExample({ fetchRandomNumber }: Props) {
const [error, setError] = useState<HttpFetchError | undefined>(undefined);
const [randomNumber, setRandomNumber] = useState<number>(0);
const [isFetching, setIsFetching] = useState<boolean>(false);
const doFetch = useCallback(async () => {
if (isFetching) return;
setIsFetching(true);
const response = await fetchRandomNumber();
if (isError(response)) {
setError(response);
} else {
setRandomNumber(response);
}
setIsFetching(false);
}, [isFetching, fetchRandomNumber]);
return (
<React.Fragment>
<EuiText>
<h2>GET example</h2>
<p>
This examples uses a simple GET route that takes no parameters or body in the request and
returns a single number.
</p>
<EuiButton
data-test-subj="routingExampleFetchRandomNumber"
disabled={isFetching}
onClick={() => doFetch()}
>
{isFetching ? <EuiLoadingSpinner /> : 'Generate a random number'}
</EuiButton>
{error !== undefined ? (
<EuiCallOut color="danger" iconType="alert">
{error}
</EuiCallOut>
) : null}
{randomNumber > -1 ? (
<h2>
Random number is <div data-test-subj="routingExampleRandomNumber">{randomNumber}</div>
</h2>
) : null}
</EuiText>
</React.Fragment>
);
}

View file

@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CoreStart, HttpFetchError } from 'kibana/public';
import {
RANDOM_NUMBER_ROUTE_PATH,
RANDOM_NUMBER_BETWEEN_ROUTE_PATH,
POST_MESSAGE_ROUTE_PATH,
INTERNAL_GET_MESSAGE_BY_ID_ROUTE,
} from '../common';
export interface Services {
fetchRandomNumber: () => Promise<number | HttpFetchError>;
fetchRandomNumberBetween: (max: number) => Promise<number | HttpFetchError>;
postMessage: (message: string, id: string) => Promise<undefined | HttpFetchError>;
getMessageById: (id: string) => Promise<string | HttpFetchError>;
addSuccessToast: (message: string) => void;
}
export function getServices(core: CoreStart): Services {
return {
addSuccessToast: (message: string) => core.notifications.toasts.addSuccess(message),
fetchRandomNumber: async () => {
try {
const response = await core.http.fetch<{ randomNumber: number }>(RANDOM_NUMBER_ROUTE_PATH);
return response.randomNumber;
} catch (e) {
return e;
}
},
fetchRandomNumberBetween: async (max: number) => {
try {
const response = await core.http.fetch<{ randomNumber: number }>(
RANDOM_NUMBER_BETWEEN_ROUTE_PATH,
{ query: { max } }
);
return response.randomNumber;
} catch (e) {
return e;
}
},
postMessage: async (message: string, id: string) => {
try {
await core.http.post(`${POST_MESSAGE_ROUTE_PATH}/${id}`, {
body: JSON.stringify({ message }),
});
} catch (e) {
return e;
}
},
getMessageById: async (id: string) => {
try {
const response = await core.http.get<{ message: string }>(
`${INTERNAL_GET_MESSAGE_BY_ID_ROUTE}/${id}`
);
return response.message;
} catch (e) {
return e;
}
},
};
}

View file

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

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreSetup, CoreStart } from 'kibana/server';
import { registerRoutes } from './routes';
export class RoutingExamplePlugin implements Plugin<{}, {}> {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
registerRoutes(router);
return {};
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

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

View file

@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { schema } from '@kbn/config-schema';
import { POST_MESSAGE_ROUTE_PATH, INTERNAL_GET_MESSAGE_BY_ID_ROUTE } from '../../common';
import { IRouter } from '../../../../src/core/server';
/**
*
* NOTE: DON'T USE IN MEMORY DATA STRUCTURES TO STORE DATA!
*
* That won't work in a system with multiple Kibanas, which is a setup we recommend for
* load balancing. I'm only doing so here to simplify the routing example. In real life,
* Elasticsearch should be used to persist data that can be shared across multiple Kibana
* instances.
*/
const messages: { [key: string]: string } = {};
/**
* @param router Pushes a message with an id onto an in memory map.
*/
export function registerPostMessageRoute(router: IRouter) {
router.post(
{
path: `${POST_MESSAGE_ROUTE_PATH}/{id}`,
validate: {
params: schema.object({
// This parameter name matches the one in POST_MESSAGE_ROUTE_PATH: `api/post_message/{id}`.
// Params are often used for ids like this.
id: schema.string(),
}),
body: schema.object({
message: schema.string({ maxLength: 100 }),
}),
},
},
async (context, request, response) => {
if (messages[request.params.id]) {
return response.badRequest({
body: `Message with id ${request.params.id} already exists`,
});
}
// See note above. NEVER DO THIS IN REAL CODE! Data should only be persisted in Elasticsearch.
messages[request.params.id] = request.body.message;
return response.ok();
}
);
}
/**
* @param router Returns the message with the given id from an in memory array.
*/
export function registerGetMessageByIdRoute(router: IRouter) {
router.get(
{
path: `${INTERNAL_GET_MESSAGE_BY_ID_ROUTE}/{id}`,
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
if (!messages[request.params.id]) {
return response.notFound();
}
return response.ok({ body: { message: messages[request.params.id] } });
}
);
}

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { schema } from '@kbn/config-schema';
import { RANDOM_NUMBER_BETWEEN_ROUTE_PATH } from '../../common';
import { IRouter } from '../../../../src/core/server';
/**
*
* @param router Registers a get route that returns a random number between one and another number suplied by the user.
*/
export function registerGetRandomNumberBetweenRoute(router: IRouter) {
router.get(
{
path: RANDOM_NUMBER_BETWEEN_ROUTE_PATH,
validate: {
query: schema.object({
max: schema.number({ defaultValue: 10 }),
}),
},
},
async (context, request, response) => {
return response.ok({
body: {
randomNumber: Math.random() * request.query.max,
},
});
}
);
}

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RANDOM_NUMBER_ROUTE_PATH } from '../../common';
import { IRouter } from '../../../../src/core/server';
/**
*
* @param router Registers a get route that returns a random number between one and ten. It has no input
* parameters, and returns a random number in the body.
*/
export function registerGetRandomNumberRoute(router: IRouter) {
router.get(
{
path: RANDOM_NUMBER_ROUTE_PATH,
validate: {},
},
async (context, request, response) => {
return response.ok({
body: {
randomNumber: Math.random() * 10,
},
});
}
);
}

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IRouter } from 'kibana/server';
import { registerGetRandomNumberRoute } from './random_number_generator';
import { registerGetRandomNumberBetweenRoute } from './random_number_between_generator';
import { registerGetMessageByIdRoute, registerPostMessageRoute } from './message_routes';
export function registerRoutes(router: IRouter) {
registerGetRandomNumberRoute(router);
registerGetRandomNumberBetweenRoute(router);
registerGetMessageByIdRoute(router);
registerPostMessageRoute(router);
}

View file

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"common/**/*.ts",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -29,6 +29,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./bfetch_explorer'),
require.resolve('./ui_actions'),
require.resolve('./state_sync'),
require.resolve('./routing'),
],
services: {
...functionalConfig.get('services'),

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('routing examples', function () {
before(async () => {
await PageObjects.common.navigateToApp('routingExample');
});
it('basic get example', async () => {
await retry.try(async () => {
await testSubjects.click('routingExampleFetchRandomNumber');
const numberAsString = await testSubjects.getVisibleText('routingExampleRandomNumber');
expect(numberAsString).to.not.be(undefined);
const number = parseFloat(numberAsString);
expect(number).to.be.lessThan(10);
expect(number).to.be.greaterThan(0);
});
});
it('basic get example with query param', async () => {
await retry.try(async () => {
await testSubjects.setValue('routingExampleMaxRandomNumberBetween', '3');
await testSubjects.click('routingExampleFetchRandomNumberBetween');
const numberAsString = await testSubjects.getVisibleText(
'routingExampleRandomNumberBetween'
);
expect(numberAsString).to.not.be(undefined);
const number = parseFloat(numberAsString);
expect(number).to.be.lessThan(3);
expect(number).to.be.greaterThan(0);
});
});
it('post and get message example', async () => {
await testSubjects.setValue('routingExampleSetMessageId', '234');
await testSubjects.setValue('routingExampleSetMessage', 'hello!');
await testSubjects.click('routingExamplePostMessage');
await testSubjects.setValue('routingExampleGetMessageId', '234');
await testSubjects.click('routingExampleFetchMessage');
await retry.try(async () => {
const message = await testSubjects.getVisibleText('routingExampleGetMessage');
expect(message).to.be('hello!');
});
});
});
}