Merge branch 'master' into feat/dynatrace-plugin-synthetics
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Change BackstageIconLinkVertical style to use pallette instead of explicit color
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Improve error messaging when passing in malformed auth
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': minor
|
||||
---
|
||||
|
||||
**BREAKING**: In order to remain interoperable with all currently supported
|
||||
deployments of Elasticsearch, this package will now conditionally use either
|
||||
the `@elastic/elasticsearch` or `@opensearch-project/opensearch` client when
|
||||
communicating with your deployed cluster.
|
||||
|
||||
If you do not rely on types exported from this package, or if you do not make
|
||||
use of the `createElasticSearchClientOptions` or `newClient` methods on the
|
||||
`ElasticSearchSearchEngine` class, then upgrading to this version should not
|
||||
require any further action on your part. Everything will continue to work as it
|
||||
always has.
|
||||
|
||||
If you _do_ rely on either of the above methods or any underlying types, some
|
||||
changes may be needed to your custom code. A type guard is now exported by this
|
||||
package (`isOpenSearchCompatible(options)`), which you may use to help clarify
|
||||
which client options are compatible with which client constructors.
|
||||
|
||||
If you are using this package with the `search.elasticsearch.provider` set to
|
||||
`aws`, and you are making use of the `newClient` method in particular, you may
|
||||
wish to start using the `@opensearch-project/opensearch` client in your custom
|
||||
code.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Added support for customizing the time format used in the `HeaderWorldClock` component
|
||||
|
||||
Here's an example of how this can be used in the `HomePage.tsx` found in `\packages\app\src\components\home` to change the clock to be in the 24hr time format:
|
||||
|
||||
```diff
|
||||
+const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
+ hour: '2-digit',
|
||||
+ minute: '2-digit',
|
||||
+ hour12: false,
|
||||
+};
|
||||
|
||||
export const homePage = (
|
||||
<Page themeId="home">
|
||||
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
|
||||
+ <HeaderWorldClock clockConfigs={clockConfigs} customTimeFormat={timeFormat} />
|
||||
</Header>
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<HomePageSearchBar />
|
||||
</Grid>
|
||||
```
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run: ls microsite/build/backstage && ls microsite/build/backstage/storybook
|
||||
|
||||
- name: Deploy both microsite and storybook to gh-pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4.3.3
|
||||
uses: JamesIves/github-pages-deploy-action@v4.3.4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: microsite/build/backstage
|
||||
|
||||
@@ -184,3 +184,4 @@ _If you're using Backstage in your organization, please try to add your company
|
||||
| [Funding Circle](https://www.fundingcircle.com/) | [Ariel Pacciaroni](https://github.com/arielpacciaroni) | We are building the internal developer portal using Backstage project and centralizing all services information at one place. The portal helps us track down repositories ownership as well as direct access to key information on every component. |
|
||||
| [Clarivate](https://www.clarivate.com) | [Gabriele Carteni](mailto:gabriele.carteni@clarivate.com) | We are building our Developer Portal using Backstage to have a better control over our software ecosystem, integrate SDLC tools and promote best practices |
|
||||
| [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. |
|
||||
| [William Hill](https://www.williamhillgroup.com/) | [Pat Mills](mailto:pat.mills@williamhill.com), [Nathan Flynn](mailto:nflynn@williamhill.co.uk), and [Nishkarsh Raj](mailto:nishkarsh.raj@williamhill.co.uk) | William Hill are leveraging Backstage to build our Engineering Portal. Our mission is to centralize the software catalog inventory to enable service discoverability, reduce the onboarding time for new Engineers, provide a single pane of glass to accelerate Developer Productivity and Save Engineers time. Our aspiration is to create an InnerSource community focussed on organization-wide patterns that are re-usable and can be self-served with the Scaffolder. |
|
||||
|
||||
@@ -80,15 +80,16 @@ const searchEngine = await ElasticSearchSearchEngine.initialize({
|
||||
const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine });
|
||||
```
|
||||
|
||||
For the engine to be available, your backend package needs a dependency into
|
||||
For the engine to be available, your backend package needs a dependency on
|
||||
package `@backstage/plugin-search-backend-module-elasticsearch`.
|
||||
|
||||
ElasticSearch needs some additional configuration before it is ready to use
|
||||
within your instance. The configuration options are documented in the
|
||||
[configuration schema definition file.](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-elasticsearch/config.d.ts)
|
||||
|
||||
The underlying functionality is using official ElasticSearch client version 7.x,
|
||||
meaning that ElasticSearch version 7 is the only one confirmed to be supported.
|
||||
The underlying functionality uses either the official ElasticSearch client
|
||||
version 7.x (meaning that ElasticSearch version 7 is the only one confirmed to
|
||||
be supported), or the OpenSearch client, when the `aws` provider is configured.
|
||||
|
||||
Should you need to create your own bespoke search experiences that require more
|
||||
than just a query translator (such as faceted search or Relay pagination), you
|
||||
@@ -97,9 +98,19 @@ search clients. The version of the client need not be the same as one used
|
||||
internally by the elastic search engine plugin. For example:
|
||||
|
||||
```typescript
|
||||
import { Client } from '@elastic/elastic-search';
|
||||
import { isOpenSearchCompatible } from '@backstage/plugin-search-backend-module-elasticsearch';
|
||||
import { Client as ElasticClient } from '@elastic/elastic-search';
|
||||
import { Client as OpenSearchClient } from '@opensearch-project/opensearch';
|
||||
|
||||
const client = searchEngine.newClient(options => new Client(options));
|
||||
const client = searchEngine.newClient(options => {
|
||||
// In reality, you would only import / instantiate one of the following, but
|
||||
// for illustrative purposes, here are both:
|
||||
if (isOpenSearchCompatible(options)) {
|
||||
return new OpenSearchClient(options);
|
||||
} else {
|
||||
return new ElasticClient(options);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Set custom index template
|
||||
|
||||
@@ -21,7 +21,7 @@ guide to do a repository-based installation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Access to a Linux-based operating system, such as Linux, MacOS or
|
||||
- Access to a Unix-based operating system, such as Linux, MacOS or
|
||||
[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)
|
||||
- An account with elevated rights to install the dependencies
|
||||
- `curl` or `wget` installed
|
||||
|
||||
+131
-93
@@ -8,6 +8,30 @@
|
||||
const React = require('react');
|
||||
const Components = require(`${process.cwd()}/core/Components.js`);
|
||||
const Block = Components.Block;
|
||||
const BulletLine = Components.BulletLine;
|
||||
|
||||
const PARTNERS = [
|
||||
{
|
||||
name: 'Frontside Software',
|
||||
url: 'https://frontside.com/backstage/',
|
||||
logo: 'img/partner-logo-frontside.png',
|
||||
},
|
||||
{
|
||||
name: 'Roadie',
|
||||
url: 'https://roadie.io/',
|
||||
logo: 'img/partner-logo-roadie.png',
|
||||
},
|
||||
{
|
||||
name: 'ThoughtWorks',
|
||||
url: 'https://www.thoughtworks.com',
|
||||
logo: 'img/partner-logo-thoughtworks.png',
|
||||
},
|
||||
{
|
||||
name: 'VMWare',
|
||||
url: 'https://www.vmware.com',
|
||||
logo: 'img/partner-logo-vmware.png',
|
||||
},
|
||||
];
|
||||
|
||||
const Background = props => {
|
||||
const { config: siteConfig } = props;
|
||||
@@ -17,14 +41,11 @@ const Background = props => {
|
||||
<Block small className="stripe-bottom bg-black-grey">
|
||||
<Block.Container style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Backstage Community</Block.Title>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.Title main>Backstage Community</Block.Title>
|
||||
<Block.Paragraph>
|
||||
What's the use of having fun if you can't share it? Exactly. Join
|
||||
the vibrant community around the Backstage project. Be it on
|
||||
GitHub, social media, Discord... You'll find a welcoming
|
||||
environment. To ensure this, we follow the{' '}
|
||||
Join the vibrant community around Backstage through social media
|
||||
and different meetups. To ensure that you have a welcoming
|
||||
environment, we follow{' '}
|
||||
<a href="https://github.com/cncf/foundation/blob/master/code-of-conduct.md">
|
||||
{' '}
|
||||
CNCF Code of Conduct
|
||||
@@ -32,110 +53,110 @@ const Background = props => {
|
||||
in everything we do.
|
||||
</Block.Paragraph>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.TextBox style={{ margin: 'auto' }}>
|
||||
<Block.Paragraph>
|
||||
Main community channels
|
||||
<br />- Chat and get support on our{' '}
|
||||
<a href="https://discord.gg/MUpMjP2">Discord</a>
|
||||
<br />- Get into contributing with the{' '}
|
||||
<a href="https://github.com/backstage/backstage/contribute">
|
||||
Good First Issues
|
||||
</a>
|
||||
<br />- Subscribe to the{' '}
|
||||
<a href="https://mailchi.mp/spotify/backstage-community">
|
||||
Community newsletter
|
||||
</a>
|
||||
<br />- Join the{' '}
|
||||
<a href="https://twitter.com/i/communities/1494019781716062215">
|
||||
Twitter community
|
||||
</a>
|
||||
<br />
|
||||
<Block.SmallTitle small>
|
||||
Get started in our community!
|
||||
</Block.SmallTitle>
|
||||
<ul>
|
||||
<li>
|
||||
<Block.Paragraph style={{ marginBottom: '0' }}>
|
||||
Chat and get support on our{' '}
|
||||
<a href="https://discord.gg/MUpMjP2">Discord</a>
|
||||
</Block.Paragraph>
|
||||
</li>
|
||||
<li>
|
||||
<Block.Paragraph style={{ marginBottom: '0' }}>
|
||||
Get into contributing with the{' '}
|
||||
<a href="https://github.com/backstage/backstage/contribute">
|
||||
Good First Issues
|
||||
</a>
|
||||
</Block.Paragraph>
|
||||
</li>
|
||||
<li>
|
||||
<Block.Paragraph style={{ marginBottom: '0' }}>
|
||||
Subscribe to the{' '}
|
||||
<a href="https://mailchi.mp/spotify/backstage-community">
|
||||
Community newsletter
|
||||
</a>
|
||||
</Block.Paragraph>
|
||||
</li>
|
||||
<li>
|
||||
<Block.Paragraph style={{ marginBottom: '0' }}>
|
||||
Join the{' '}
|
||||
<a href="https://twitter.com/i/communities/1494019781716062215">
|
||||
Twitter community
|
||||
</a>
|
||||
</Block.Paragraph>
|
||||
</li>
|
||||
</ul>
|
||||
</Block.Paragraph>
|
||||
</Block.TextBox>
|
||||
</Block.Container>
|
||||
</Block>
|
||||
|
||||
<Block small className="stripe bg-black">
|
||||
<Block.Container style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Backstage Community Sessions</Block.Title>
|
||||
<Block.Paragraph>
|
||||
Missed a meetup? Wondering when the next one is coming up? We've
|
||||
got you covered! Check out our all-new Meetups page.
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="/on-demand">Meetups</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Adopter Community Sessions</Block.Title>
|
||||
<Block.Paragraph>
|
||||
Backstage Community Sessions is the monthly meetup where we all
|
||||
come together to listen to the latest maintainer updates, learn
|
||||
from each other about adopting, share exciting new demos or
|
||||
discuss any relevant topic like developer effectiveness, developer
|
||||
experience, developer portals, etc. Have something to share, or a
|
||||
burning question? Add it to the{' '}
|
||||
<a href="https://github.com/backstage/community/issues">issue</a>.
|
||||
</Block.Paragraph>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Contributor Community Sessions</Block.Title>
|
||||
<Block.Paragraph>
|
||||
Discuss all things contributing, diving deep under the hood of
|
||||
Backstage (Backstage of Backstage? Backerstage?). An open
|
||||
discussion with maintainers and contributors of Backstage. If you
|
||||
like Backstage, this is your favorite Zoom meeting of the month,
|
||||
guaranteed! Have something to share, or a burning question? Add it
|
||||
to the{' '}
|
||||
<a href="https://github.com/backstage/community/issues">issue</a>.
|
||||
</Block.Paragraph>
|
||||
<Block className="stripe-top stripe-bottom bg-teal-bottom">
|
||||
<Block.Container style={{ flexFlow: 'column nowrap' }}>
|
||||
<Block.TextBox wide>
|
||||
<Block.Subtitle>Offical Backstage initiatives</Block.Subtitle>
|
||||
<Block.Title small>
|
||||
Stay tuned to the latest developments
|
||||
</Block.Title>
|
||||
</Block.TextBox>
|
||||
<Block.Container>
|
||||
<Block.TextBox style={{ flexShrink: '1' }}>
|
||||
<BulletLine />
|
||||
<Block.SmallTitle small>Community sessions</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
Maintainers and adopters meet monthly to share updates, demos,
|
||||
and ideas. Yep, all sessions are recorded!
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="/on-demand">
|
||||
Join a session
|
||||
</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox style={{ flexShrink: '1' }}>
|
||||
<BulletLine />
|
||||
<Block.SmallTitle small>Newsletter</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
The official monthly Backstage newsletter. Don't miss the latest
|
||||
news from your favorite project!
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="https://mailchi.mp/spotify/backstage-community">
|
||||
Subscribe
|
||||
</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox style={{ flexShrink: '1' }}>
|
||||
<BulletLine />
|
||||
<Block.SmallTitle small>Contributor Spotlight</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
A recognition for valuable community work. Nominate contributing
|
||||
members for their efforts! We'll put them in the spotlight ❤️
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="/nominate">Nominate now</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
</Block.Container>
|
||||
</Block.Container>
|
||||
</Block>
|
||||
|
||||
<Block small className="stripe bg-black-grey">
|
||||
<Block.Container style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Backstage official Newsletter</Block.Title>
|
||||
<Block className="stripe-top bg-black">
|
||||
<Block.Container wrapped style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox wide>
|
||||
<Block.Subtitle>Community initiatives</Block.Subtitle>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.TextBox small>
|
||||
<Block.SmallTitle>Open Mic Meetup</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
The official monthly Backstage newsletter. Containing the latest
|
||||
news from your favorite project.
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="https://mailchi.mp/spotify/backstage-community">
|
||||
Subscribe
|
||||
</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
</Block.Container>
|
||||
</Block>
|
||||
|
||||
<Block className="stripe bg-black">
|
||||
<Block.Container style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Spotlight</Block.Title>
|
||||
<Block.Paragraph>
|
||||
A recognition for valuable community work, the{' '}
|
||||
<b>Contributor Spotlight</b>. Nominate contributing members for
|
||||
their efforts! We'll put them in the spotlight ❤️.
|
||||
<br />
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="nominate ">Nominate now</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Open Mic Meetup</Block.Title>
|
||||
<Block.Paragraph>
|
||||
A monthly casual get together of Backstage users sharing their
|
||||
experiences and helping each other. Hosted by{' '}
|
||||
A casual get together of Backstage users sharing their experiences
|
||||
and helping each other. Hosted by{' '}
|
||||
<a href="https://roadie.io/">Roadie.io</a> and{' '}
|
||||
<a href="https://frontside.com/">Frontside Software</a>.
|
||||
<br />
|
||||
</Block.Paragraph>
|
||||
<Block.LinkButton href="https://backstage-openmic.com/">
|
||||
Learn more
|
||||
</Block.LinkButton>
|
||||
</Block.TextBox>
|
||||
<Block.TextBox>
|
||||
<Block.Title>Backstage Weekly</Block.Title>
|
||||
<Block.TextBox small>
|
||||
<Block.SmallTitle>Backstage Weekly Newsletter</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
A weekly newsletter with news, updates and things community from
|
||||
your friends at <a href="https://roadie.io/">Roadie.io</a>.
|
||||
@@ -146,6 +167,23 @@ const Background = props => {
|
||||
</Block.TextBox>
|
||||
</Block.Container>
|
||||
</Block>
|
||||
|
||||
<Block className="stripe-top bg-black-grey">
|
||||
<Block.Container wrapped style={{ justifyContent: 'flex-start' }}>
|
||||
<Block.TextBox wide>
|
||||
<Block.Subtitle>Commercial Partners</Block.Subtitle>
|
||||
</Block.TextBox>
|
||||
{PARTNERS.map(partner => (
|
||||
<Block.TextBox small>
|
||||
<Block.SmallTitle>
|
||||
<a href={partner.url}>
|
||||
<img src={`${baseUrl}${partner.logo}`} alt={partner.name} />
|
||||
</a>
|
||||
</Block.SmallTitle>
|
||||
</Block.TextBox>
|
||||
))}
|
||||
</Block.Container>
|
||||
</Block>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -49,10 +49,19 @@ const clockConfigs: ClockConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
export const homePage = (
|
||||
<Page themeId="home">
|
||||
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
|
||||
<HeaderWorldClock clockConfigs={clockConfigs} />
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
|
||||
@@ -47,7 +47,7 @@ const useIconStyles = makeStyles(
|
||||
textAlign: 'center',
|
||||
},
|
||||
disabled: {
|
||||
color: 'gray',
|
||||
color: theme.palette.text.secondary,
|
||||
cursor: 'default',
|
||||
},
|
||||
primary: {
|
||||
|
||||
@@ -59,9 +59,10 @@ export function createCardExtension<T>(options: {
|
||||
name?: string;
|
||||
}): Extension<(props: CardExtensionProps<T>) => JSX.Element>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const HeaderWorldClock: (props: {
|
||||
clockConfigs: ClockConfig[];
|
||||
customTimeFormat?: Intl.DateTimeFormatOptions | undefined;
|
||||
}) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -16,5 +16,3 @@
|
||||
|
||||
export { HomepageCompositionRoot } from './HomepageCompositionRoot';
|
||||
export { SettingsModal } from './SettingsModal';
|
||||
export { HeaderWorldClock } from './HeaderWorldClock';
|
||||
export type { ClockConfig } from './HeaderWorldClock';
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 { Header } from '@backstage/core-components';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { ClockConfig, HeaderWorldClock } from './HeaderWorldClock';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Home/Components/HeaderWorldClock',
|
||||
decorators: [(Story: ComponentType<{}>) => wrapInTestApp(<Story />)],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const clockConfigs: ClockConfig[] = [
|
||||
{
|
||||
label: 'NYC',
|
||||
timeZone: 'America/New_York',
|
||||
},
|
||||
{
|
||||
label: 'UTC',
|
||||
timeZone: 'UTC',
|
||||
},
|
||||
{
|
||||
label: 'STO',
|
||||
timeZone: 'Europe/Stockholm',
|
||||
},
|
||||
{
|
||||
label: 'TYO',
|
||||
timeZone: 'Asia/Tokyo',
|
||||
},
|
||||
];
|
||||
|
||||
const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Header title="Header World Clock" pageTitleOverride="Home">
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwentyFourHourClocks = () => {
|
||||
const clockConfigs: ClockConfig[] = [
|
||||
{
|
||||
label: 'NYC',
|
||||
timeZone: 'America/New_York',
|
||||
},
|
||||
{
|
||||
label: 'UTC',
|
||||
timeZone: 'UTC',
|
||||
},
|
||||
{
|
||||
label: 'STO',
|
||||
timeZone: 'Europe/Stockholm',
|
||||
},
|
||||
{
|
||||
label: 'TYO',
|
||||
timeZone: 'Asia/Tokyo',
|
||||
},
|
||||
];
|
||||
|
||||
const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Header title="24hr Header World Clock" pageTitleOverride="Home">
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
+29
@@ -86,3 +86,32 @@ describe('HeaderWorldClock with invalid Time Zone', () => {
|
||||
expect(rendered.getByText('GMT')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeaderWorldClock with custom Time Format', () => {
|
||||
it('uses 24hr clock from custom Time Format', async () => {
|
||||
const clockConfigs: ClockConfig[] = [
|
||||
{
|
||||
label: 'UTC',
|
||||
timeZone: 'UTC',
|
||||
},
|
||||
];
|
||||
|
||||
const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const currentTime = `${new Date().getHours()}:${new Date().getMinutes()}`;
|
||||
expect(rendered.getByText(currentTime)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+44
-8
@@ -32,7 +32,10 @@ export type ClockConfig = {
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
function getTimes(clockConfigs: ClockConfig[]) {
|
||||
function getTimes(
|
||||
clockConfigs: ClockConfig[],
|
||||
customTimeFormat?: Intl.DateTimeFormatOptions,
|
||||
) {
|
||||
const d = new Date();
|
||||
const lang = window.navigator.language;
|
||||
|
||||
@@ -47,7 +50,7 @@ function getTimes(clockConfigs: ClockConfig[]) {
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
timeZone: clockConfig.timeZone,
|
||||
...timeFormat,
|
||||
...(customTimeFormat ?? timeFormat),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -68,24 +71,57 @@ function getTimes(clockConfigs: ClockConfig[]) {
|
||||
return clocks;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const HeaderWorldClock = (props: { clockConfigs: ClockConfig[] }) => {
|
||||
const { clockConfigs } = props;
|
||||
/**
|
||||
* A component to display a configurable list of clocks for various time zones.
|
||||
*
|
||||
* @example
|
||||
* Here's a simple example:
|
||||
* ```
|
||||
* // This will give you a clock for the time zone that Stockholm is in
|
||||
* // you can add more than one but keep in mind space may be limited
|
||||
* const clockConfigs: ClockConfig[] = [
|
||||
* {
|
||||
* label: 'STO',
|
||||
* timeZone: 'Europe/Stockholm',
|
||||
* },
|
||||
* ];
|
||||
*
|
||||
* // Setting hour12 to false will make all the clocks show in the 24hr format
|
||||
* const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
* hour: '2-digit',
|
||||
* minute: '2-digit',
|
||||
* hour12: false,
|
||||
* };
|
||||
*
|
||||
* // Here is the component in use:
|
||||
* <HeaderWorldClock
|
||||
* clockConfigs={clockConfigs}
|
||||
* customTimeFormat={timeFormat}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderWorldClock = (props: {
|
||||
clockConfigs: ClockConfig[];
|
||||
customTimeFormat?: Intl.DateTimeFormatOptions;
|
||||
}) => {
|
||||
const { clockConfigs, customTimeFormat } = props;
|
||||
|
||||
const defaultTimes: TimeObj[] = [];
|
||||
const [clocks, setTimes] = React.useState(defaultTimes);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimes(getTimes(clockConfigs));
|
||||
setTimes(getTimes(clockConfigs, customTimeFormat));
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setTimes(getTimes(clockConfigs));
|
||||
setTimes(getTimes(clockConfigs, customTimeFormat));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [clockConfigs]);
|
||||
}, [clockConfigs, customTimeFormat]);
|
||||
|
||||
if (clocks.length !== 0) {
|
||||
return (
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './CompanyLogo';
|
||||
export * from './Toolkit';
|
||||
export type { ClockConfig } from './HeaderWorldClock';
|
||||
|
||||
@@ -31,9 +31,10 @@ export {
|
||||
ComponentTabs,
|
||||
ComponentTab,
|
||||
WelcomeTitle,
|
||||
HeaderWorldClock,
|
||||
} from './plugin';
|
||||
export { SettingsModal, HeaderWorldClock } from './components';
|
||||
export { SettingsModal } from './components';
|
||||
export * from './assets';
|
||||
export type { ClockConfig } from './components';
|
||||
export type { ClockConfig } from './homePageComponents';
|
||||
export { createCardExtension } from './extensions';
|
||||
export type { ComponentRenderer } from './extensions';
|
||||
|
||||
@@ -136,3 +136,20 @@ export const HomePageStarredEntities = homePlugin.provide(
|
||||
components: () => import('./homePageComponents/StarredEntities'),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* A component to display a configurable list of clocks for various time zones.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderWorldClock = homePlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'HeaderWorldClock',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./homePageComponents/HeaderWorldClock').then(
|
||||
m => m.HeaderWorldClock,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -23,13 +23,14 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config, JsonObject } from '@backstage/config';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import {
|
||||
TemplateEntityV1beta3,
|
||||
TaskSpec,
|
||||
templateEntityV1beta3Validator,
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { validate } from 'jsonschema';
|
||||
@@ -431,17 +432,36 @@ function parseBearerToken(header?: string): {
|
||||
token?: string;
|
||||
entityRef?: string;
|
||||
} {
|
||||
const token = header?.match(/Bearer\s+(\S+)/i)?.[1];
|
||||
if (!header) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!token) return {};
|
||||
try {
|
||||
const token = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)?.[1];
|
||||
if (!token) {
|
||||
throw new TypeError('Expected Bearer with JWT');
|
||||
}
|
||||
|
||||
const [_header, rawPayload, _signature] = token.split('.');
|
||||
const payload: { sub: string } = JSON.parse(
|
||||
Buffer.from(rawPayload, 'base64').toString(),
|
||||
);
|
||||
const [_header, rawPayload, _signature] = token.split('.');
|
||||
const payload: JsonValue = JSON.parse(
|
||||
Buffer.from(rawPayload, 'base64').toString(),
|
||||
);
|
||||
|
||||
return {
|
||||
entityRef: payload.sub,
|
||||
token,
|
||||
};
|
||||
if (
|
||||
typeof payload !== 'object' ||
|
||||
payload === null ||
|
||||
Array.isArray(payload)
|
||||
) {
|
||||
throw new TypeError('Malformed JWT payload');
|
||||
}
|
||||
|
||||
const sub = payload.sub;
|
||||
if (typeof sub !== 'string') {
|
||||
throw new TypeError('Expected string sub claim');
|
||||
}
|
||||
|
||||
return { entityRef: sub, token };
|
||||
} catch (e) {
|
||||
throw new InputError(`Invalid authorization header: ${stringifyError(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,60 +5,27 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { ApiResponse } from '@opensearch-project/opensearch';
|
||||
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { BulkHelper } from '@opensearch-project/opensearch/lib/Helpers';
|
||||
import { BulkStats } from '@opensearch-project/opensearch/lib/Helpers';
|
||||
import { Config } from '@backstage/config';
|
||||
import type { ConnectionOptions } from 'tls';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { IndexableResultSet } from '@backstage/plugin-search-common';
|
||||
import { Logger } from 'winston';
|
||||
import { Readable } from 'stream';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
import { SearchQuery } from '@backstage/plugin-search-common';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchAgentOptions {
|
||||
// (undocumented)
|
||||
keepAlive?: boolean;
|
||||
// (undocumented)
|
||||
keepAliveMsecs?: number;
|
||||
// (undocumented)
|
||||
maxFreeSockets?: number;
|
||||
// (undocumented)
|
||||
maxSockets?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
import { TransportRequestPromise } from '@opensearch-project/opensearch/lib/Transport';
|
||||
|
||||
// @public
|
||||
export interface ElasticSearchClientOptions {
|
||||
export interface BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
|
||||
// (undocumented)
|
||||
auth?: ElasticSearchAuth;
|
||||
// (undocumented)
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
compression?: 'gzip';
|
||||
// (undocumented)
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
||||
// (undocumented)
|
||||
enableMetaHeader?: boolean;
|
||||
@@ -69,28 +36,14 @@ export interface ElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
name?: string | symbol;
|
||||
// (undocumented)
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodeFilter?: (connection: any) => boolean;
|
||||
// (undocumented)
|
||||
nodes?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodeSelector?: ((connections: any[]) => any) | string;
|
||||
// (undocumented)
|
||||
opaqueIdPrefix?: string;
|
||||
// (undocumented)
|
||||
pingTimeout?: number;
|
||||
// (undocumented)
|
||||
provider?: 'aws' | 'elastic';
|
||||
// (undocumented)
|
||||
proxy?: string | URL;
|
||||
// (undocumented)
|
||||
requestTimeout?: number;
|
||||
@@ -112,6 +65,118 @@ export interface ElasticSearchClientOptions {
|
||||
Transport?: ElasticSearchTransportConstructor;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchAgentOptions {
|
||||
// (undocumented)
|
||||
keepAlive?: boolean;
|
||||
// (undocumented)
|
||||
keepAliveMsecs?: number;
|
||||
// (undocumented)
|
||||
maxFreeSockets?: number;
|
||||
// (undocumented)
|
||||
maxSockets?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAliasAction =
|
||||
| {
|
||||
remove: {
|
||||
index: any;
|
||||
alias: any;
|
||||
};
|
||||
add?: undefined;
|
||||
}
|
||||
| {
|
||||
add: {
|
||||
indices: any;
|
||||
alias: any;
|
||||
index?: undefined;
|
||||
};
|
||||
remove?: undefined;
|
||||
}
|
||||
| {
|
||||
add: {
|
||||
index: any;
|
||||
alias: any;
|
||||
indices?: undefined;
|
||||
};
|
||||
remove?: undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAuth =
|
||||
| OpenSearchAuth
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ElasticSearchClientOptions =
|
||||
| ElasticSearchElasticSearchClientOptions
|
||||
| OpenSearchElasticSearchClientOptions;
|
||||
|
||||
// @public
|
||||
export class ElasticSearchClientWrapper {
|
||||
// (undocumented)
|
||||
bulk(bulkOptions: {
|
||||
datasource: Readable;
|
||||
onDocument: () => ElasticSearchIndexAction;
|
||||
refreshOnCompletion?: string | boolean;
|
||||
}): BulkHelper<BulkStats>;
|
||||
// (undocumented)
|
||||
createIndex({
|
||||
index,
|
||||
}: {
|
||||
index: string;
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
deleteIndex({
|
||||
index,
|
||||
}: {
|
||||
index: string | string[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
static fromClientOptions(
|
||||
options: ElasticSearchClientOptions,
|
||||
): ElasticSearchClientWrapper;
|
||||
// (undocumented)
|
||||
getAliases({
|
||||
aliases,
|
||||
}: {
|
||||
aliases: string[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
indexExists({
|
||||
index,
|
||||
}: {
|
||||
index: string | string[];
|
||||
}): TransportRequestPromise<ApiResponse<boolean, unknown>>;
|
||||
// (undocumented)
|
||||
putIndexTemplate(
|
||||
template: ElasticSearchCustomIndexTemplate,
|
||||
): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
search({
|
||||
index,
|
||||
body,
|
||||
}: {
|
||||
index: string | string[];
|
||||
body: Object;
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
// (undocumented)
|
||||
updateAliases({
|
||||
actions,
|
||||
}: {
|
||||
actions: ElasticSearchAliasAction[];
|
||||
}): TransportRequestPromise<ApiResponse<Record<string, any>, unknown>>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ElasticSearchConcreteQuery = {
|
||||
documentTypes?: string[];
|
||||
@@ -150,6 +215,35 @@ export type ElasticSearchCustomIndexTemplateBody = {
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ElasticSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
auth?: ElasticSearchAuth;
|
||||
// (undocumented)
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodes?:
|
||||
| string
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
provider?: 'elastic';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchHighlightConfig = {
|
||||
fragmentDelimiter: string;
|
||||
@@ -166,6 +260,14 @@ export type ElasticSearchHighlightOptions = {
|
||||
numFragments?: number;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchIndexAction = {
|
||||
index: {
|
||||
_index: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchNodeOptions {
|
||||
// (undocumented)
|
||||
@@ -258,7 +360,7 @@ export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
indexSeparator: string;
|
||||
alias: string;
|
||||
logger: Logger;
|
||||
elasticSearchClient: Client;
|
||||
elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -273,4 +375,67 @@ export interface ElasticSearchTransportConstructor {
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export const isOpenSearchCompatible: (
|
||||
opts: ElasticSearchClientOptions,
|
||||
) => opts is OpenSearchElasticSearchClientOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
export type OpenSearchAuth = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OpenSearchConnectionConstructor {
|
||||
// (undocumented)
|
||||
new (opts?: any): any;
|
||||
// (undocumented)
|
||||
roles: {
|
||||
MASTER: string;
|
||||
DATA: string;
|
||||
INGEST: string;
|
||||
};
|
||||
// (undocumented)
|
||||
statuses: {
|
||||
ALIVE: string;
|
||||
DEAD: string;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OpenSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
// (undocumented)
|
||||
auth?: OpenSearchAuth;
|
||||
// (undocumented)
|
||||
connection?: OpenSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
node?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
nodes?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
// (undocumented)
|
||||
provider?: 'aws';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OpenSearchNodeOptions {
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions;
|
||||
// (undocumented)
|
||||
headers?: Record<string, any>;
|
||||
// (undocumented)
|
||||
id?: string;
|
||||
// (undocumented)
|
||||
roles?: {
|
||||
master: boolean;
|
||||
data: boolean;
|
||||
ingest: boolean;
|
||||
};
|
||||
// (undocumented)
|
||||
ssl?: ConnectionOptions;
|
||||
// (undocumented)
|
||||
url: URL;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,11 +23,12 @@
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@acuris/aws-es-connection": "^2.2.0",
|
||||
"@backstage/config": "^1.0.1",
|
||||
"@backstage/plugin-search-backend-node": "^0.6.3-next.0",
|
||||
"@backstage/plugin-search-common": "^0.3.5",
|
||||
"@elastic/elasticsearch": "7.13.0",
|
||||
"@elastic/elasticsearch": "^7.13.0",
|
||||
"@opensearch-project/opensearch": "^1.1.0",
|
||||
"aws-os-connection": "^0.1.0",
|
||||
"aws-sdk": "^2.948.0",
|
||||
"elastic-builder": "^2.16.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -37,7 +38,8 @@
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.14.1-next.0",
|
||||
"@backstage/cli": "^0.17.3-next.0",
|
||||
"@elastic/elasticsearch-mock": "^1.0.0"
|
||||
"@elastic/elasticsearch-mock": "^1.0.0",
|
||||
"@short.io/opensearch-mock": "^0.3.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
+109
-19
@@ -16,17 +16,60 @@
|
||||
import type { ConnectionOptions as TLSConnectionOptions } from 'tls';
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client and
|
||||
* are what will be passed as an argument to the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
||||
* but are maintained separately so that this interface is not coupled to
|
||||
* Typeguard to differentiate ElasticSearch client options which are compatible
|
||||
* with OpenSearch vs. ElasticSearch clients. Useful when calling the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchClientOptions {
|
||||
provider?: 'aws' | 'elastic';
|
||||
export const isOpenSearchCompatible = (
|
||||
opts: ElasticSearchClientOptions,
|
||||
): opts is OpenSearchElasticSearchClientOptions => {
|
||||
return opts?.provider === 'aws';
|
||||
};
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client or the
|
||||
* `@opensearch-project/opensearch` client, depending on the given config. It
|
||||
* will be passed as an argument to the
|
||||
* {@link ElasticSearchSearchEngine.newClient} method.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchClientOptions =
|
||||
| ElasticSearchElasticSearchClientOptions
|
||||
| OpenSearchElasticSearchClientOptions;
|
||||
|
||||
/**
|
||||
* Options used to configure the `@opensearch-project/opensearch` client.
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@opensearch-project/opensearch`,
|
||||
* but are maintained separately so that this interface is not coupled to it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
provider?: 'aws';
|
||||
auth?: OpenSearchAuth;
|
||||
connection?: OpenSearchConnectionConstructor;
|
||||
node?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
nodes?: string | string[] | OpenSearchNodeOptions | OpenSearchNodeOptions[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client.
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
||||
* but are maintained separately so that this interface is not coupled to it.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchElasticSearchClientOptions
|
||||
extends BaseElasticSearchClientOptions {
|
||||
provider?: 'elastic';
|
||||
auth?: ElasticSearchAuth;
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
node?:
|
||||
| string
|
||||
| string[]
|
||||
@@ -37,8 +80,21 @@ export interface ElasticSearchClientOptions {
|
||||
| string[]
|
||||
| ElasticSearchNodeOptions
|
||||
| ElasticSearchNodeOptions[];
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base client options that are shared across `@opensearch-project/opensearch`
|
||||
* and `@elastic/elasticsearch` clients.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface BaseElasticSearchClientOptions {
|
||||
Transport?: ElasticSearchTransportConstructor;
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
maxRetries?: number;
|
||||
requestTimeout?: number;
|
||||
pingTimeout?: number;
|
||||
@@ -56,25 +112,24 @@ export interface ElasticSearchClientOptions {
|
||||
headers?: Record<string, any>;
|
||||
opaqueIdPrefix?: string;
|
||||
name?: string | symbol;
|
||||
auth?: ElasticSearchAuth;
|
||||
proxy?: string | URL;
|
||||
enableMetaHeader?: boolean;
|
||||
cloud?: {
|
||||
id: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type OpenSearchAuth = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| OpenSearchAuth
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
@@ -101,6 +156,22 @@ export interface ElasticSearchNodeOptions {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchNodeOptions {
|
||||
url: URL;
|
||||
id?: string;
|
||||
agent?: ElasticSearchAgentOptions;
|
||||
ssl?: TLSConnectionOptions;
|
||||
headers?: Record<string, any>;
|
||||
roles?: {
|
||||
master: boolean;
|
||||
data: boolean;
|
||||
ingest: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -127,6 +198,23 @@ export interface ElasticSearchConnectionConstructor {
|
||||
ML: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface OpenSearchConnectionConstructor {
|
||||
new (opts?: any): any;
|
||||
statuses: {
|
||||
ALIVE: string;
|
||||
DEAD: string;
|
||||
};
|
||||
roles: {
|
||||
MASTER: string;
|
||||
DATA: string;
|
||||
INGEST: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -139,3 +227,5 @@ export interface ElasticSearchTransportConstructor {
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
|
||||
// todo(iamEAP) implement canary types to ensure we remain compatible through upgrades.
|
||||
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 { ConfigReader } from '@backstage/config';
|
||||
import { Client as ElasticSearchClient } from '@elastic/elasticsearch';
|
||||
import { Client as OpenSearchClient } from '@opensearch-project/opensearch';
|
||||
import Mock from '@short.io/opensearch-mock';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
import {
|
||||
createElasticSearchClientOptions,
|
||||
ElasticSearchClientOptions,
|
||||
} from './ElasticSearchSearchEngine';
|
||||
|
||||
jest.mock('@elastic/elasticsearch', () => ({
|
||||
...jest.requireActual('@elastic/elasticsearch'),
|
||||
Client: jest.fn().mockReturnValue({
|
||||
search: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
cat: {
|
||||
aliases: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
},
|
||||
helpers: {
|
||||
bulk: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
},
|
||||
indices: {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
delete: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
exists: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
putIndexTemplate: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
updateAliases: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'es', args })),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@opensearch-project/opensearch', () => ({
|
||||
...jest.requireActual('@opensearch-project/opensearch'),
|
||||
Client: jest.fn().mockReturnValue({
|
||||
search: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
cat: {
|
||||
aliases: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
},
|
||||
helpers: {
|
||||
bulk: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
},
|
||||
indices: {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
delete: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
exists: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
putIndexTemplate: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
updateAliases: jest
|
||||
.fn()
|
||||
.mockImplementation(async args => ({ client: 'os', args })),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ElasticSearchClientWrapper', () => {
|
||||
describe('elasticSearchClient', () => {
|
||||
let esOptions: ElasticSearchClientOptions;
|
||||
|
||||
beforeEach(async () => {
|
||||
esOptions = await createElasticSearchClientOptions(
|
||||
new ConfigReader({
|
||||
node: 'http://localhost:9200',
|
||||
}),
|
||||
);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('instantiation', () => {
|
||||
ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
expect(ElasticSearchClient).toHaveBeenCalledWith(esOptions);
|
||||
});
|
||||
|
||||
it('search', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const searchInput = { index: 'xyz', body: { eg: 'etc' } };
|
||||
const result = (await wrapper.search(searchInput)) as any;
|
||||
|
||||
// Should call the ElasticSearch client's search with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(searchInput);
|
||||
});
|
||||
|
||||
it('bulk', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const bulkInput = {
|
||||
datasource: Readable.from([{}]),
|
||||
onDocument() {
|
||||
return { index: { _index: 'xyz' } };
|
||||
},
|
||||
refreshCompletion: 'xyz',
|
||||
};
|
||||
const result = (await wrapper.bulk(bulkInput)) as any;
|
||||
|
||||
// Should call the ElasticSearch client's bulk helper with expected input
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(bulkInput);
|
||||
});
|
||||
|
||||
it('putIndexTemplate', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const indexTemplate = { name: 'xyz', body: { index_patterns: ['*'] } };
|
||||
const result = (await wrapper.putIndexTemplate(indexTemplate)) as any;
|
||||
|
||||
// Should call the ElasticSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(indexTemplate);
|
||||
});
|
||||
|
||||
it('indexExists', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.indexExists(input)) as any;
|
||||
|
||||
// Should call the ElasticSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('deleteIndex', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.deleteIndex(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('createIndex', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.createIndex(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('getAliases', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const input = { aliases: ['xyz'] };
|
||||
const result = (await wrapper.getAliases(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual({
|
||||
format: 'json',
|
||||
name: input.aliases,
|
||||
});
|
||||
});
|
||||
|
||||
it('updateAliases', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(esOptions);
|
||||
|
||||
const input = { actions: [{ remove: { index: 'xyz', alias: 'abc' } }] };
|
||||
const result = (await wrapper.updateAliases(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('es');
|
||||
expect(result.args).toStrictEqual({
|
||||
body: { actions: input.actions },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('openSearchClient', () => {
|
||||
const mock = new Mock();
|
||||
let osOptions: ElasticSearchClientOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
osOptions = {
|
||||
provider: 'aws',
|
||||
node: 'https://my-es-cluster.eu-west-1.es.amazonaws.com',
|
||||
connection: mock.getConnection(),
|
||||
};
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('instantiation', () => {
|
||||
ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
expect(OpenSearchClient).toHaveBeenCalledWith(osOptions);
|
||||
});
|
||||
|
||||
it('search', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const searchInput = { index: 'xyz', body: { eg: 'etc' } };
|
||||
const result = (await wrapper.search(searchInput)) as any;
|
||||
|
||||
// Should call the OpenSearch client's search with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(searchInput);
|
||||
});
|
||||
|
||||
it('bulk', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const bulkInput = {
|
||||
datasource: Readable.from([{}]),
|
||||
onDocument() {
|
||||
return { index: { _index: 'xyz' } };
|
||||
},
|
||||
refreshCompletion: 'xyz',
|
||||
};
|
||||
const result = (await wrapper.bulk(bulkInput)) as any;
|
||||
|
||||
// Should call the OpenSearch client's bulk helper with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(bulkInput);
|
||||
});
|
||||
|
||||
it('putIndexTemplate', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const indexTemplate = { name: 'xyz', body: { index_patterns: ['*'] } };
|
||||
const result = (await wrapper.putIndexTemplate(indexTemplate)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(indexTemplate);
|
||||
});
|
||||
|
||||
it('indexExists', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.indexExists(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('deleteIndex', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.deleteIndex(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('createIndex', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const input = { index: 'xyz' };
|
||||
const result = (await wrapper.createIndex(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual(input);
|
||||
});
|
||||
|
||||
it('getAliases', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const input = { aliases: ['xyz'] };
|
||||
const result = (await wrapper.getAliases(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual({
|
||||
format: 'json',
|
||||
name: input.aliases,
|
||||
});
|
||||
});
|
||||
|
||||
it('updateAliases', async () => {
|
||||
const wrapper = ElasticSearchClientWrapper.fromClientOptions(osOptions);
|
||||
|
||||
const input = { actions: [{ remove: { index: 'xyz', alias: 'abc' } }] };
|
||||
const result = (await wrapper.updateAliases(input)) as any;
|
||||
|
||||
// Should call the OpenSearch client with expected input.
|
||||
expect(result.client).toBe('os');
|
||||
expect(result.args).toStrictEqual({
|
||||
body: { actions: input.actions },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed 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 { Client as ElasticSearchClient } from '@elastic/elasticsearch';
|
||||
import { Client as OpenSearchClient } from '@opensearch-project/opensearch';
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
ElasticSearchClientOptions,
|
||||
isOpenSearchCompatible,
|
||||
} from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchCustomIndexTemplate } from './types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchAliasAction =
|
||||
| {
|
||||
remove: { index: any; alias: any };
|
||||
add?: undefined;
|
||||
}
|
||||
| {
|
||||
add: { indices: any; alias: any; index?: undefined };
|
||||
remove?: undefined;
|
||||
}
|
||||
| {
|
||||
add: { index: any; alias: any; indices?: undefined };
|
||||
remove?: undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchIndexAction = {
|
||||
index: {
|
||||
_index: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper class that exposes logical methods that are conditionally fired
|
||||
* against either a configured Elasticsearch client or a configured Opensearch
|
||||
* client.
|
||||
*
|
||||
* This is necessary because, despite its intention to be API-compatible, the
|
||||
* opensearch client does not support API key-based authentication. This is
|
||||
* also the sanest way to accomplish this while making typescript happy.
|
||||
*
|
||||
* In the future, if the differences between implementations become
|
||||
* unmaintainably divergent, we should split out the Opensearch and
|
||||
* Elasticsearch search engine implementations.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ElasticSearchClientWrapper {
|
||||
private readonly elasticSearchClient: ElasticSearchClient | undefined;
|
||||
private readonly openSearchClient: OpenSearchClient | undefined;
|
||||
|
||||
private constructor({
|
||||
openSearchClient,
|
||||
elasticSearchClient,
|
||||
}: {
|
||||
openSearchClient?: OpenSearchClient;
|
||||
elasticSearchClient?: ElasticSearchClient;
|
||||
}) {
|
||||
this.openSearchClient = openSearchClient;
|
||||
this.elasticSearchClient = elasticSearchClient;
|
||||
}
|
||||
|
||||
static fromClientOptions(options: ElasticSearchClientOptions) {
|
||||
if (isOpenSearchCompatible(options)) {
|
||||
return new ElasticSearchClientWrapper({
|
||||
openSearchClient: new OpenSearchClient(options),
|
||||
});
|
||||
}
|
||||
|
||||
return new ElasticSearchClientWrapper({
|
||||
elasticSearchClient: new ElasticSearchClient(options),
|
||||
});
|
||||
}
|
||||
|
||||
search({ index, body }: { index: string | string[]; body: Object }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.search({ index, body });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.search({ index, body });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
bulk(bulkOptions: {
|
||||
datasource: Readable;
|
||||
onDocument: () => ElasticSearchIndexAction;
|
||||
refreshOnCompletion?: string | boolean;
|
||||
}) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.helpers.bulk(bulkOptions);
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.helpers.bulk(bulkOptions);
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
putIndexTemplate(template: ElasticSearchCustomIndexTemplate) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.putIndexTemplate(template);
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.putIndexTemplate(template);
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
indexExists({ index }: { index: string | string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.exists({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.exists({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
deleteIndex({ index }: { index: string | string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.delete({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.delete({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
createIndex({ index }: { index: string }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.create({ index });
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.create({ index });
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
getAliases({ aliases }: { aliases: string[] }) {
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: aliases,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: aliases,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
|
||||
updateAliases({ actions }: { actions: ElasticSearchAliasAction[] }) {
|
||||
const filteredActions = actions.filter(Boolean);
|
||||
if (this.openSearchClient) {
|
||||
return this.openSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: filteredActions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.elasticSearchClient) {
|
||||
return this.elasticSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: filteredActions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('No client defined');
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -16,8 +16,9 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Client, errors } from '@elastic/elasticsearch';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import Mock from '@elastic/elasticsearch-mock';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
import {
|
||||
ElasticSearchConcreteQuery,
|
||||
decodePageCursor,
|
||||
@@ -70,7 +71,7 @@ const customIndexTemplate = {
|
||||
describe('ElasticSearchSearchEngine', () => {
|
||||
let testSearchEngine: ElasticSearchSearchEngine;
|
||||
let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests;
|
||||
let client: Client;
|
||||
let clientWrapper: ElasticSearchClientWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
testSearchEngine = new ElasticSearchSearchEngine(
|
||||
@@ -86,7 +87,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
getVoidLogger(),
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
client = testSearchEngine['elasticSearchClient'];
|
||||
clientWrapper = testSearchEngine['elasticSearchClientWrapper'];
|
||||
});
|
||||
|
||||
describe('custom index template', () => {
|
||||
@@ -686,7 +687,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should handle index/search type filtering correctly', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: 'testTerm',
|
||||
filters: {},
|
||||
@@ -725,7 +726,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should create matchAll query if no term defined', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: '',
|
||||
filters: {},
|
||||
@@ -759,7 +760,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
});
|
||||
|
||||
it('should query only specified indices if defined', async () => {
|
||||
const elasticSearchQuerySpy = jest.spyOn(client, 'search');
|
||||
const elasticSearchQuerySpy = jest.spyOn(clientWrapper, 'search');
|
||||
await testSearchEngine.query({
|
||||
term: '',
|
||||
filters: {},
|
||||
@@ -809,7 +810,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
type: 'test-index',
|
||||
indexPrefix: '',
|
||||
indexSeparator: '-index__',
|
||||
elasticSearchClient: client,
|
||||
elasticSearchClientWrapper: clientWrapper,
|
||||
}),
|
||||
);
|
||||
expect(indexerMock.on).toHaveBeenCalledWith(
|
||||
|
||||
+14
-46
@@ -14,10 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
awsGetCredentials,
|
||||
createAWSConnection,
|
||||
} from '@acuris/aws-es-connection';
|
||||
import { awsGetCredentials, createAWSConnection } from 'aws-os-connection';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
IndexableDocument,
|
||||
@@ -26,47 +23,17 @@ import {
|
||||
SearchEngine,
|
||||
SearchQuery,
|
||||
} from '@backstage/plugin-search-common';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import esb from 'elastic-builder';
|
||||
import { isEmpty, isNaN as nan, isNumber } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchClientOptions } from './ElasticSearchClientOptions';
|
||||
import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer';
|
||||
import { ElasticSearchCustomIndexTemplate } from './types';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
|
||||
export type { ElasticSearchClientOptions };
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplate = {
|
||||
name: string;
|
||||
body: ElasticSearchCustomIndexTemplateBody;
|
||||
};
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template body
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplateBody = {
|
||||
/**
|
||||
* Array of wildcard (*) expressions used to match the names of data streams and indices during creation.
|
||||
*/
|
||||
index_patterns: string[];
|
||||
/**
|
||||
* An ordered list of component template names.
|
||||
* Component templates are merged in the order specified,
|
||||
* meaning that the last component template specified has the highest precedence.
|
||||
*/
|
||||
composed_of?: string[];
|
||||
/**
|
||||
* See available properties of template
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body
|
||||
*/
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search query that the elasticsearch engine understands.
|
||||
* @public
|
||||
@@ -143,7 +110,7 @@ function isBlank(str: string) {
|
||||
* @public
|
||||
*/
|
||||
export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
private readonly elasticSearchClient: Client;
|
||||
private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
private readonly highlightOptions: ElasticSearchHighlightConfig;
|
||||
|
||||
constructor(
|
||||
@@ -153,7 +120,8 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
private readonly logger: Logger,
|
||||
highlightOptions?: ElasticSearchHighlightOptions,
|
||||
) {
|
||||
this.elasticSearchClient = this.newClient(options => new Client(options));
|
||||
this.elasticSearchClientWrapper =
|
||||
ElasticSearchClientWrapper.fromClientOptions(elasticSearchClientOptions);
|
||||
const uuidTag = uuid();
|
||||
this.highlightOptions = {
|
||||
preTag: `<${uuidTag}>`,
|
||||
@@ -177,7 +145,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
if (options.provider === 'elastic') {
|
||||
logger.info('Initializing Elastic.co ElasticSearch search engine.');
|
||||
} else if (options.provider === 'aws') {
|
||||
logger.info('Initializing AWS ElasticSearch search engine.');
|
||||
logger.info('Initializing AWS OpenSearch search engine.');
|
||||
} else {
|
||||
logger.info('Initializing ElasticSearch search engine.');
|
||||
}
|
||||
@@ -269,7 +237,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
|
||||
async setIndexTemplate(template: ElasticSearchCustomIndexTemplate) {
|
||||
try {
|
||||
await this.elasticSearchClient.indices.putIndexTemplate(template);
|
||||
await this.elasticSearchClientWrapper.putIndexTemplate(template);
|
||||
this.logger.info('Custom index template set');
|
||||
} catch (error) {
|
||||
this.logger.error(`Unable to set custom index template: ${error}`);
|
||||
@@ -284,7 +252,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
indexPrefix: this.indexPrefix,
|
||||
indexSeparator: this.indexSeparator,
|
||||
alias,
|
||||
elasticSearchClient: this.elasticSearchClient,
|
||||
elasticSearchClientWrapper: this.elasticSearchClientWrapper,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
@@ -292,13 +260,13 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
indexer.on('error', async e => {
|
||||
this.logger.error(`Failed to index documents for type ${type}`, e);
|
||||
try {
|
||||
const response = await this.elasticSearchClient.indices.exists({
|
||||
const response = await this.elasticSearchClientWrapper.indexExists({
|
||||
index: indexer.indexName,
|
||||
});
|
||||
const indexCreated = response.body;
|
||||
if (indexCreated) {
|
||||
this.logger.info(`Removing created index ${indexer.indexName}`);
|
||||
await this.elasticSearchClient.indices.delete({
|
||||
await this.elasticSearchClientWrapper.deleteIndex({
|
||||
index: indexer.indexName,
|
||||
});
|
||||
}
|
||||
@@ -319,7 +287,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
? documentTypes.map(it => this.constructSearchAlias(it))
|
||||
: this.constructSearchAlias('*');
|
||||
try {
|
||||
const result = await this.elasticSearchClient.search({
|
||||
const result = await this.elasticSearchClientWrapper.search({
|
||||
index: queryIndices,
|
||||
body: elasticSearchQuery,
|
||||
});
|
||||
@@ -398,7 +366,7 @@ export function encodePageCursor({ page }: { page: number }): string {
|
||||
return Buffer.from(`${page}`, 'utf-8').toString('base64');
|
||||
}
|
||||
|
||||
async function createElasticSearchClientOptions(
|
||||
export async function createElasticSearchClientOptions(
|
||||
config?: Config,
|
||||
): Promise<ElasticSearchClientOptions> {
|
||||
if (!config) {
|
||||
|
||||
+3
-3
@@ -16,13 +16,13 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestPipeline } from '@backstage/plugin-search-backend-node';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import Mock from '@elastic/elasticsearch-mock';
|
||||
import { range } from 'lodash';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer';
|
||||
|
||||
const mock = new Mock();
|
||||
const client = new Client({
|
||||
const clientWrapper = ElasticSearchClientWrapper.fromClientOptions({
|
||||
node: 'http://localhost:9200',
|
||||
Connection: mock.getConnection(),
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('ElasticSearchSearchEngineIndexer', () => {
|
||||
indexSeparator: '-index__',
|
||||
alias: 'some-type-index__search',
|
||||
logger: getVoidLogger(),
|
||||
elasticSearchClient: client,
|
||||
elasticSearchClientWrapper: clientWrapper,
|
||||
});
|
||||
|
||||
// Set up all requisite Elastic mocks.
|
||||
|
||||
+26
-29
@@ -16,9 +16,9 @@
|
||||
|
||||
import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { ElasticSearchClientWrapper } from './ElasticSearchClientWrapper';
|
||||
|
||||
/**
|
||||
* Options for instansiate ElasticSearchSearchEngineIndexer
|
||||
@@ -30,7 +30,7 @@ export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
indexSeparator: string;
|
||||
alias: string;
|
||||
logger: Logger;
|
||||
elasticSearchClient: Client;
|
||||
elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
};
|
||||
|
||||
function duration(startTimestamp: [number, number]): string {
|
||||
@@ -57,7 +57,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
private readonly removableAlias: string;
|
||||
private readonly logger: Logger;
|
||||
private readonly sourceStream: Readable;
|
||||
private readonly elasticSearchClient: Client;
|
||||
private readonly elasticSearchClientWrapper: ElasticSearchClientWrapper;
|
||||
private bulkResult: Promise<any>;
|
||||
|
||||
constructor(options: ElasticSearchSearchEngineIndexerOptions) {
|
||||
@@ -70,7 +70,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
this.indexName = this.constructIndexName(`${Date.now()}`);
|
||||
this.alias = options.alias;
|
||||
this.removableAlias = `${this.alias}_removable`;
|
||||
this.elasticSearchClient = options.elasticSearchClient;
|
||||
this.elasticSearchClientWrapper = options.elasticSearchClientWrapper;
|
||||
|
||||
// The ES client bulk helper supports stream-based indexing, but we have to
|
||||
// supply the stream directly to it at instantiation-time. We can't supply
|
||||
@@ -83,7 +83,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
|
||||
// Keep a reference to the ES Bulk helper so that we can know when all
|
||||
// documents have been successfully written to ES.
|
||||
this.bulkResult = this.elasticSearchClient.helpers.bulk({
|
||||
this.bulkResult = this.elasticSearchClientWrapper.bulk({
|
||||
datasource: this.sourceStream,
|
||||
onDocument() {
|
||||
that.processed++;
|
||||
@@ -98,16 +98,15 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
async initialize(): Promise<void> {
|
||||
this.logger.info(`Started indexing documents for index ${this.type}`);
|
||||
|
||||
const aliases = await this.elasticSearchClient.cat.aliases({
|
||||
format: 'json',
|
||||
name: [this.alias, this.removableAlias],
|
||||
const aliases = await this.elasticSearchClientWrapper.getAliases({
|
||||
aliases: [this.alias, this.removableAlias],
|
||||
});
|
||||
|
||||
this.removableIndices = [
|
||||
...new Set(aliases.body.map((r: Record<string, any>) => r.index)),
|
||||
] as string[];
|
||||
|
||||
await this.elasticSearchClient.indices.create({
|
||||
await this.elasticSearchClientWrapper.createIndex({
|
||||
index: this.indexName,
|
||||
});
|
||||
}
|
||||
@@ -140,25 +139,23 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
)}`,
|
||||
result,
|
||||
);
|
||||
await this.elasticSearchClient.indices.updateAliases({
|
||||
body: {
|
||||
actions: [
|
||||
{
|
||||
remove: { index: this.constructIndexName('*'), alias: this.alias },
|
||||
},
|
||||
this.removableIndices.length
|
||||
? {
|
||||
add: {
|
||||
indices: this.removableIndices,
|
||||
alias: this.removableAlias,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
add: { index: this.indexName, alias: this.alias },
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
await this.elasticSearchClientWrapper.updateAliases({
|
||||
actions: [
|
||||
{
|
||||
remove: { index: this.constructIndexName('*'), alias: this.alias },
|
||||
},
|
||||
this.removableIndices.length
|
||||
? {
|
||||
add: {
|
||||
indices: this.removableIndices,
|
||||
alias: this.removableAlias,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
add: { index: this.indexName, alias: this.alias },
|
||||
},
|
||||
].filter(Boolean),
|
||||
});
|
||||
|
||||
// If any indices are removable, remove them. Do not bubble up this error,
|
||||
@@ -166,7 +163,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
if (this.removableIndices.length) {
|
||||
this.logger.info('Removing stale search indices', this.removableIndices);
|
||||
try {
|
||||
await this.elasticSearchClient.indices.delete({
|
||||
await this.elasticSearchClientWrapper.deleteIndex({
|
||||
index: this.removableIndices,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -15,13 +15,25 @@
|
||||
*/
|
||||
|
||||
export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine';
|
||||
export { isOpenSearchCompatible } from './ElasticSearchClientOptions';
|
||||
export type {
|
||||
BaseElasticSearchClientOptions,
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
ElasticSearchElasticSearchClientOptions,
|
||||
ElasticSearchTransportConstructor,
|
||||
ElasticSearchNodeOptions,
|
||||
ElasticSearchAuth,
|
||||
OpenSearchAuth,
|
||||
OpenSearchConnectionConstructor,
|
||||
OpenSearchElasticSearchClientOptions,
|
||||
OpenSearchNodeOptions,
|
||||
} from './ElasticSearchClientOptions';
|
||||
export type {
|
||||
ElasticSearchAliasAction,
|
||||
ElasticSearchClientWrapper,
|
||||
ElasticSearchIndexAction,
|
||||
} from './ElasticSearchClientWrapper';
|
||||
export type {
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
@@ -30,10 +42,12 @@ export type {
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchOptions,
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
} from './ElasticSearchSearchEngine';
|
||||
export type {
|
||||
ElasticSearchSearchEngineIndexer,
|
||||
ElasticSearchSearchEngineIndexerOptions,
|
||||
} from './ElasticSearchSearchEngineIndexer';
|
||||
export type {
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
} from './types';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplate = {
|
||||
name: string;
|
||||
body: ElasticSearchCustomIndexTemplateBody;
|
||||
};
|
||||
|
||||
/**
|
||||
* Elasticsearch specific index template body
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchCustomIndexTemplateBody = {
|
||||
/**
|
||||
* Array of wildcard (*) expressions used to match the names of data streams and indices during creation.
|
||||
*/
|
||||
index_patterns: string[];
|
||||
/**
|
||||
* An ordered list of component template names.
|
||||
* Component templates are merged in the order specified,
|
||||
* meaning that the last component template specified has the highest precedence.
|
||||
*/
|
||||
composed_of?: string[];
|
||||
/**
|
||||
* See available properties of template
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/7.15/indices-put-template.html#put-index-template-api-request-body
|
||||
*/
|
||||
template?: Record<string, any>;
|
||||
};
|
||||
@@ -20,13 +20,18 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { ElasticSearchSearchEngine } from './engines';
|
||||
export { ElasticSearchSearchEngine, isOpenSearchCompatible } from './engines';
|
||||
export type {
|
||||
BaseElasticSearchClientOptions,
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchAliasAction,
|
||||
ElasticSearchClientWrapper,
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
ElasticSearchElasticSearchClientOptions,
|
||||
ElasticSearchHighlightConfig,
|
||||
ElasticSearchHighlightOptions,
|
||||
ElasticSearchIndexAction,
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
@@ -38,4 +43,8 @@ export type {
|
||||
ElasticSearchSearchEngineIndexerOptions,
|
||||
ElasticSearchCustomIndexTemplate,
|
||||
ElasticSearchCustomIndexTemplateBody,
|
||||
OpenSearchAuth,
|
||||
OpenSearchConnectionConstructor,
|
||||
OpenSearchElasticSearchClientOptions,
|
||||
OpenSearchNodeOptions,
|
||||
} from './engines';
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@acuris/aws-es-connection@^2.2.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/@acuris/aws-es-connection/-/aws-es-connection-2.3.0.tgz#bfa736163bdfe2e7fc12f8529eead949fb4e73fe"
|
||||
integrity sha512-k8jSkdNwpSPty7oautGaJXoAzzPjP7WKbmaAE9i+vJrKWg9n2UtjesHeDhOAPzV9MG8EzXofGozYjaLdeTP+Ug==
|
||||
dependencies:
|
||||
aws4 "^1.11.0"
|
||||
|
||||
"@ampproject/remapping@^2.1.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34"
|
||||
@@ -2079,10 +2072,10 @@
|
||||
find-my-way "^4.3.3"
|
||||
into-stream "^6.0.0"
|
||||
|
||||
"@elastic/elasticsearch@7.13.0":
|
||||
version "7.13.0"
|
||||
resolved "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.13.0.tgz#6dcf511dfa91187e22c81e54f41f4bd0fd96b4d6"
|
||||
integrity sha512-WgwLWo2p9P2tdqzBGX9fHeG8p5IOTXprXNTECQG2mJ7z9n93N5AFBJpEw4d35tWWeCWi9jI13A2wzQZH7XZ/xw==
|
||||
"@elastic/elasticsearch@^7.13.0":
|
||||
version "7.17.0"
|
||||
resolved "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.17.0.tgz#589fb219234cf1b0da23744e82b1d25e2fe9a797"
|
||||
integrity sha512-5QLPCjd0uLmLj1lSuKSThjNpq39f6NmlTy9ROLFwG5gjyTgpwSqufDeYG/Fm43Xs05uF7WcscoO7eguI3HuuYA==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
hpagent "^0.1.1"
|
||||
@@ -5127,6 +5120,16 @@
|
||||
rxjs "7.5.5"
|
||||
tslib "2.0.3"
|
||||
|
||||
"@opensearch-project/opensearch@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-1.1.0.tgz#8b3c8b4cbcea01755ba092d2997bf0b4ca7f22f7"
|
||||
integrity sha512-1TDw92JL8rD1b2QGluqBsIBLIiD5SGciIpz4qkrGAe9tcdfQ1ptub5e677rhWl35UULSjr6hP8M6HmISZ/M5HQ==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
hpagent "^0.1.1"
|
||||
ms "^2.1.3"
|
||||
secure-json-parse "^2.4.0"
|
||||
|
||||
"@opentelemetry/api@^1.0.1":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz#a167e46c10d05a07ab299fc518793b0cff8f6924"
|
||||
@@ -5203,9 +5206,9 @@
|
||||
"@react-hookz/deep-equal" "^1.0.1"
|
||||
|
||||
"@react-hookz/web@^14.0.0":
|
||||
version "14.2.2"
|
||||
resolved "https://registry.npmjs.org/@react-hookz/web/-/web-14.2.2.tgz#eee0085f954e5b62d0a6c5b20d8786c28abfb9ad"
|
||||
integrity sha512-6FeZHKV3FFOYnbSJMkkqXDoMuIX0HEFJlDF4LpHR7RsA3zrH2xzhtF8nSYZ+pG6fDIqYKID9yQuILFmbAWaqCQ==
|
||||
version "14.3.0"
|
||||
resolved "https://registry.npmjs.org/@react-hookz/web/-/web-14.3.0.tgz#ccfcfa65aa1e56ca5a3b258a9bd987e0daa67bae"
|
||||
integrity sha512-lmZ9rAoHbBFZo0v72cjHMCY/hz9Wp6WQd/Kx53A+HumxkAaJT3AuBkd9jJgrtdsNiHdG0IEr93/oOza3+G6kVA==
|
||||
dependencies:
|
||||
"@react-hookz/deep-equal" "^1.0.2"
|
||||
|
||||
@@ -5420,6 +5423,15 @@
|
||||
dependencies:
|
||||
any-observable "^0.3.0"
|
||||
|
||||
"@short.io/opensearch-mock@^0.3.1":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.npmjs.org/@short.io/opensearch-mock/-/opensearch-mock-0.3.1.tgz#41678404b94342ac60263a8df590f9b1efa80622"
|
||||
integrity sha512-gwLTzxJrZNGVQNn+FHLhpw7at3jHb6tNczq2hvFPJ3C3nLaCBRkbP2IQHrIDzklLBVFV83Ug0Xgs73A5o9eLtg==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
find-my-way "^4.3.3"
|
||||
into-stream "^6.0.0"
|
||||
|
||||
"@sideway/address@^4.1.0":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz#9e321e74310963fdf8eebfbee09c7bd69972de4d"
|
||||
@@ -8250,6 +8262,13 @@ available-typed-arrays@^1.0.2:
|
||||
dependencies:
|
||||
array-filter "^1.0.0"
|
||||
|
||||
aws-os-connection@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/aws-os-connection/-/aws-os-connection-0.1.0.tgz#02c1be08cf63448c5bdc9d47b03b7ad3222b662c"
|
||||
integrity sha512-WBN2S/5zgzfPpEiOTEW7IPJX+hlq+AQeAVKK4AL/Th0SfRRKu6a46i4aZKovQslBkKnmcSFvy+mHSJ299N8YWA==
|
||||
dependencies:
|
||||
aws4 "^1.11.0"
|
||||
|
||||
aws-sdk-mock@^5.2.1:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.7.0.tgz#b2a9454c78d008186c3f1a460b79cdb9cc9dfdc4"
|
||||
@@ -9245,9 +9264,9 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001286:
|
||||
integrity sha512-5v7LFQU4Sb/qvkz7JcZkvtSH1Ko+1x2kgo3ocdBeMGZSOFpuE1kkm0kpTwLtWeFrw5qw08ulLxJjVIXIS8MkiQ==
|
||||
|
||||
canvas@^2.6.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz#58ec841cba36cef0675bc7a74ebd1561f0b476b0"
|
||||
integrity sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==
|
||||
version "2.9.3"
|
||||
resolved "https://registry.npmjs.org/canvas/-/canvas-2.9.3.tgz#8723c4f970442d4cdcedba5221579f9660a58bdb"
|
||||
integrity sha512-WOUM7ghii5TV2rbhaZkh1youv/vW1/Canev6Yx6BG2W+1S07w8jKZqKkPnbiPpQEDsnJdN8ouDd7OvQEGXDcUw==
|
||||
dependencies:
|
||||
"@mapbox/node-pre-gyp" "^1.0.0"
|
||||
nan "^2.15.0"
|
||||
|
||||
Reference in New Issue
Block a user