Merge branch 'master' into master
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Use of `SidebarContext` has been deprecated and will be removed in a future release. Instead, `useSidebarOpenState()` should be used to consume the context and `<SidebarOpenStateProvider>` should be used to provide it.
|
||||
|
||||
To prepare your app, update `packages/app/src/components/Root/Root.tsx` as follows:
|
||||
|
||||
```diff
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
- SidebarContext
|
||||
SidebarDivider,
|
||||
// ...
|
||||
SidebarSpace,
|
||||
+ useSidebarOpenState,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
// ...
|
||||
|
||||
const SidebarLogo = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
- const { isOpen } = useContext(SidebarContext);
|
||||
+ const { isOpen } = useSidebarOpenState();
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights': patch
|
||||
---
|
||||
|
||||
Export TechInsightsClient so it may be extended by custom implementations
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
'@techdocs/cli': patch
|
||||
---
|
||||
|
||||
Updated sidebar-related logic to use `<SidebarPinStateProvider>` + `useSidebarPinState()` and/or `<SidebarOpenStateProvider>` + `useSidebarOpenState()` from `@backstage/core-components`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Introducing a `NewlineDelimitedJsonCollatorFactory`, which can be used to create search indices from newline delimited JSON files stored in external storage readable via a configured `UrlReader` instance.
|
||||
|
||||
This is useful if you have an independent process periodically generating `*.ndjson` files consisting of `IndexableDocument` objects and want to be able to generate a fresh index based on the latest version of such a file.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Fixed a bug that prevented `TestPipeline.withSubject` from identifying valid `Readable` subjects that were technically transform streams.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
The `SidebarPinStateContext` and `SidebarContext` have been deprecated and will be removed in a future release. Instead, use `<SidebarPinStateProvider>` + `useSidebarPinState()` and/or `<SidebarOpenStateProvider>` + `useSidebarOpenState()`.
|
||||
|
||||
This was done to ensure that sidebar state can be shared successfully across components exported by different packages, regardless of what version of this package is resolved and installed for each individual package.
|
||||
@@ -132,4 +132,5 @@ _If you're using Backstage in your organization, please try to add your company
|
||||
|[The Warehouse Group](https://www.thewarehouse.co.nz)|[Matt Law](mailto:matt.law@thewarehouse.co.nz)|Backstage enables us to bootstrap our middleware environment of new services for our Dev teams in a matter of seconds. CI, CD, testing, logging, deployments are all taken care of to get them up and running in less than 60 seconds.
|
||||
| [Tink](https://tink.com/) | [Sebastian Olsson](https://github.com/Sebelino), [Błażej Szum](https://github.com/blazejszumtink), [Anders Eurenius Runvald](https://github.com/anders-er-at-tink) | Internal developer portal which provides templates for creating new Java or Go microservices seamlessly. Also includes a tech radar and a visualization of our CD pipeline. |
|
||||
| [Brandwatch](https://brandwatch.com)| [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API.
|
||||
| [Laybuy](https://www.laybuy.com)| [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuy’s new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards.
|
||||
| [Sendinblue](https://engineering.sendinblue.com/)| [Tanguy Antoine](mailto:antoine.tanguy@sendinblue.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness |
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
id: integrating-search-into-plugins
|
||||
title: Integrating Search into a plugin
|
||||
description: How to integrate Search into a Backstage plugin
|
||||
---
|
||||
|
||||
The Backstage Search Platform was designed to give plugin developers the APIs
|
||||
and interfaces needed to offer search experiences within their plugins, while
|
||||
abstracting away (and instead empowering application integrators to choose) the
|
||||
specific underlying search technologies.
|
||||
|
||||
On this page, you'll find concepts and tutorials for leveraging the Backstage
|
||||
Search Platform in your plugin.
|
||||
|
||||
## Providing data to the search platform
|
||||
|
||||
> A guide on how to create collators is coming soon!
|
||||
|
||||
## Building a search experience into your plugin
|
||||
|
||||
While the core Search plugin offers components and extensions that empower app
|
||||
integrators to compose a global search experience, you may find that you want a
|
||||
narrower search experience just within your plugin. This could be as literal as
|
||||
an autocomplete-style search bar focused on documents provided by your plugin
|
||||
(for example, the [TechDocsSearch](https://github.com/backstage/backstage/blob/master/plugins/techdocs/src/search/components/TechDocsSearch.tsx)
|
||||
component), or as abstract as a widget that presents a list of links that
|
||||
are contextually related to something else on the page.
|
||||
|
||||
### Search Experience Concepts
|
||||
|
||||
Knowing these high-level concepts will help you as you craft your in-plugin
|
||||
search experience.
|
||||
|
||||
- All search experiences must be wrapped in a `<SearchContextProvider>`, which
|
||||
is provided by `@backstage/plugin-search-react`. This context keeps track
|
||||
of state necessary to perform search queries and display any results. As
|
||||
inputs to the query are updated (e.g. a `term` or `filter` values), the
|
||||
updated query is executed and `results` are refreshed. Check out the
|
||||
[SearchContextValue](https://backstage.io/docs/reference/plugin-search-react.searchcontextvalue)
|
||||
for details.
|
||||
- The aforementioned state can be modified and/or consumed via the
|
||||
`useSearch()` hook, also exported by `@backstage/plugin-search-react`.
|
||||
- For more literal search experiences, reusable components are available
|
||||
to import and compose into a cohesive experience in your plugin (e.g.
|
||||
`<SearchBar />` or `<SearchFilter.Checkbox />`). You can see all such
|
||||
components in [Backstage's storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchbar--default).
|
||||
|
||||
### Search Experience Tutorials
|
||||
|
||||
The following tutorials make use of packages and plugins that you may not yet
|
||||
have as dependencies for your plugin; be sure to add them before you use them!
|
||||
|
||||
- [`@backstage/plugin-search-react`](https://www.npmjs.com/package/@backstage/plugin-search-react) - A
|
||||
package containing components, hooks, and types that are shared across all
|
||||
frontend plugins, including plugins like yours!
|
||||
- [`@backstage/plugin-search`](https://www.npmjs.com/package/@backstage/plugin-search) - The
|
||||
main search plugin, used by app integrators to compose global search
|
||||
experiences. <!-- todo(@backstage/techdocs-core): remove all references to @backstage/plugin-search once #11676 is resolved -->
|
||||
- [`@backstage/core-components`](https://www.npmjs.com/package/@backstage/core-components) - A
|
||||
package containing generic components useful for a variety of experiences
|
||||
built in Backstage.
|
||||
|
||||
#### Improved "404" page experience
|
||||
|
||||
Imagine you have a plugin that allows users to manage _widgets_. Perhaps they
|
||||
can be viewed at a URL like `backstage.example.biz/widgets/{widgetName}`.
|
||||
At some point, a widget is renamed, and links to that widget's page from
|
||||
chat systems, wikis, or browser bookmarks become stale, resulting in errors or
|
||||
404s.
|
||||
|
||||
What if instead of showing a broken page or the generic "looks like someone
|
||||
dropped the mic" 404 page, you showed a list of possibly related widgets?
|
||||
|
||||
```javascript
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { SearchResult } from '@backstage/plugin-search';
|
||||
import { SearchContextProvider } from '@backstage/plugin-search-react';
|
||||
|
||||
export const Widget404Page = ({ widgetName }) => {
|
||||
// Supplying this to <SearchContextProvider> runs a pre-filtered search with
|
||||
// the given widgetName as the search term, focused on search result of type
|
||||
// "widget" with no other filters.
|
||||
const preFiltered = {
|
||||
term: widgetName,
|
||||
types: ['widget'],
|
||||
filters: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContextProvider initialState={preFiltered}>
|
||||
{/* The <SearchResult> component allows us to iterate through results and
|
||||
display them in whatever way fits best! */}
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
{results.map(({ document }) => (
|
||||
<Link to={document.location} key={document.location}>
|
||||
{document.title}
|
||||
</Link>
|
||||
))}
|
||||
)}
|
||||
<SearchResult>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
);
|
||||
```
|
||||
|
||||
Not all search experiences require user input! As you can see, it's possible to
|
||||
leverage the Backstage Search Platform's frontend framework without necessarily
|
||||
giving users input controls.
|
||||
|
||||
#### Simple search page
|
||||
|
||||
Of course, it's also possible to provide a more fully featured search
|
||||
experience in your plugin. The simplest way is to leverage reusable components
|
||||
provided by the `@backstage/plugin-search` package, like this:
|
||||
|
||||
```javascript
|
||||
import { useProfile } from '@internal/api';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
PageWithHeader,
|
||||
} from '@backstage/core-components';
|
||||
import { SearchBar, SearchResult } from '@backstage/plugin-search';
|
||||
import { SearchContextProvider } from '@backstage/plugin-search-react';
|
||||
|
||||
export const ManageMyWidgets = () => {
|
||||
const { primaryTeam } = useProfile();
|
||||
// In this example, note how we are pre-filtering results down to a specific
|
||||
// owner field value (the currently logged-in user's team), but allowing the
|
||||
// search term to be controlled by the user via the <SearchBar /> component.
|
||||
const preFiltered = {
|
||||
types: ['widget'],
|
||||
term: '',
|
||||
filters: {
|
||||
owner: primaryTeam,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWithHeader title="Widgets Home">
|
||||
<Content>
|
||||
<ContentHeader title="All your Widgets and More" />
|
||||
<SearchContextProvider initialState={preFiltered}>
|
||||
<SearchBar />
|
||||
<SearchResult>
|
||||
{/* Render results here, just like above */}
|
||||
</SearchResult>
|
||||
</SearchContextProvider>
|
||||
</Content>
|
||||
</PageWithHeader>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Custom search control surfaces
|
||||
|
||||
If the reusable search components provided by `@backstage/plugin-search` aren't
|
||||
adequate, no problem! There's an API in place that you can use to author your
|
||||
own components to control the various parts of the search context.
|
||||
|
||||
```javascript
|
||||
import { useSearch } from '@backstage/plugin-search-react';
|
||||
import ChipInput from 'material-ui-chip-input';
|
||||
|
||||
export const CustomChipFilter = ({ name }) => {
|
||||
const { filters, setFilters } = useSearch();
|
||||
const chipValues = filters[name] || [];
|
||||
|
||||
// When a chip value is changed, update the filters value by calling the
|
||||
// setFilters function from the search context.
|
||||
const handleChipChange = (chip, index) => {
|
||||
// There may be filters set for other fields. Be sure to maintain them.
|
||||
setFilters(prevState => {
|
||||
const { [name]: filter = [], ...others } = prevState;
|
||||
|
||||
if (index === undefined) {
|
||||
filter.push(chip);
|
||||
} else {
|
||||
filter.splice(index, 1);
|
||||
}
|
||||
|
||||
return { ...others, [name]: filter };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ChipInput
|
||||
value={chipValues}
|
||||
onAdd={handleChipChange}
|
||||
onDelete={handleChipChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Check out the [SearchContextValue type](https://github.com/backstage/backstage/blob/master/plugins/search-react/src/context/SearchContext.tsx)
|
||||
for more details on what methods and values are available for manipulating and
|
||||
reading the search context.
|
||||
|
||||
If you produce something generic and reusable, consider contributing your
|
||||
component upstream so that all users of the Backstage Search Platform can
|
||||
benefit. Issues and pull requests welcome.
|
||||
@@ -3,7 +3,7 @@ title: Cloudify
|
||||
author: Cloudify
|
||||
authorUrl: https://cloudify.co/
|
||||
category: Orchestration
|
||||
description: Load blueprints from desired Cloudify Manager instance
|
||||
documentation: https://github.com/Cloudify-PS/backstage-cloudify-plugin#readme
|
||||
description: Cloudify provides a remote execution and environment management backend that handles the provisioning and continuous update of Kubernetes, Terraform, Ansible, CloudFormation, Azure ARM, VRO based environments through a single API endpoint.
|
||||
documentation: https://github.com/cloudify-cosmo/backstage-cloudify-plugin#readme
|
||||
iconUrl: https://avatars.githubusercontent.com/u/6260555?s=200&v=4
|
||||
npmPackageName: 'plugin-cloudify'
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"plugins/plugin-development",
|
||||
"plugins/structure-of-a-plugin",
|
||||
"plugins/integrating-plugin-into-software-catalog",
|
||||
"plugins/integrating-search-into-plugins",
|
||||
"plugins/composability",
|
||||
"plugins/analytics",
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, PropsWithChildren } from 'react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Link, makeStyles } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
@@ -39,13 +39,13 @@ import { Shortcuts } from '@backstage/plugin-shortcuts';
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
} from '@backstage/core-components';
|
||||
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
|
||||
import GroupIcon from '@material-ui/icons/People';
|
||||
@@ -68,7 +68,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
|
||||
const SidebarLogo = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
Header,
|
||||
Lifecycle,
|
||||
Page,
|
||||
SidebarPinStateContext,
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
import { useSearch } from '@backstage/plugin-search-react';
|
||||
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
|
||||
import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core';
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
bar: {
|
||||
@@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||
|
||||
const SearchPage = () => {
|
||||
const classes = useStyles();
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
const { types } = useSearch();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import { CardHeaderProps } from '@material-ui/core/CardHeader';
|
||||
import { Column } from '@material-table/core';
|
||||
import { ComponentClass } from 'react';
|
||||
import { ComponentProps } from 'react';
|
||||
import { Context } from 'react';
|
||||
import { default as CSS_2 } from 'csstype';
|
||||
import { CSSProperties } from 'react';
|
||||
import { ElementType } from 'react';
|
||||
@@ -903,14 +902,10 @@ export const sidebarConfig: {
|
||||
mobileSidebarHeight: number;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const SidebarContext: Context<SidebarContextType>;
|
||||
// @public @deprecated
|
||||
export const SidebarContext: React_2.Context<SidebarContextType>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
@@ -983,6 +978,21 @@ export type SidebarItemClassKey =
|
||||
| 'arrows'
|
||||
| 'selected';
|
||||
|
||||
// @public
|
||||
export type SidebarOpenState = {
|
||||
isOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const SidebarOpenStateProvider: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
value: SidebarOpenState;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type SidebarOptions = {
|
||||
drawerWidthClosed?: number;
|
||||
@@ -1005,15 +1015,31 @@ export type SidebarPageProps = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SidebarPinState = {
|
||||
isPinned: boolean;
|
||||
toggleSidebarPinState: () => any;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export const SidebarPinStateContext: React_2.Context<SidebarPinStateContextType>;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type SidebarPinStateContextType = {
|
||||
isPinned: boolean;
|
||||
toggleSidebarPinState: () => any;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const SidebarPinStateProvider: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
value: SidebarPinStateContextType;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type SidebarProps = {
|
||||
openDelayMs?: number;
|
||||
@@ -1447,6 +1473,12 @@ export class UserIdentity implements IdentityApi {
|
||||
signOut(): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const useSidebarOpenState: () => SidebarOpenState;
|
||||
|
||||
// @public
|
||||
export const useSidebarPinState: () => SidebarPinState;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@backstage/core-plugin-api": "^1.0.2",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@backstage/version-bridge": "^1.0.1",
|
||||
"@material-table/core": "^3.1.0",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { makeStyles, ThemeProvider } from '@material-ui/core/styles';
|
||||
import { SidebarPinStateContext } from '../Sidebar/Page';
|
||||
import { useSidebarPinState } from '../Sidebar/SidebarPinStateContext';
|
||||
|
||||
export type PageClassKey = 'root';
|
||||
|
||||
@@ -43,7 +43,7 @@ type Props = {
|
||||
|
||||
export function Page(props: Props) {
|
||||
const { themeId, children } = props;
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
const classes = useStyles({ isMobile });
|
||||
return (
|
||||
<ThemeProvider
|
||||
|
||||
@@ -27,14 +27,14 @@ import {
|
||||
SidebarExpandButton,
|
||||
SidebarItem,
|
||||
SidebarSearchField,
|
||||
SidebarPinStateContext,
|
||||
SidebarPinStateProvider,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
} from '.';
|
||||
|
||||
async function renderScalableSidebar() {
|
||||
await renderInTestApp(
|
||||
<SidebarPinStateContext.Provider
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned: false,
|
||||
isMobile: false,
|
||||
@@ -75,7 +75,7 @@ async function renderScalableSidebar() {
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
<SidebarExpandButton />
|
||||
</Sidebar>
|
||||
</SidebarPinStateContext.Provider>,
|
||||
</SidebarPinStateProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,15 @@ import {
|
||||
makeSidebarConfig,
|
||||
makeSidebarSubmenuConfig,
|
||||
SidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarConfigContext,
|
||||
SubmenuConfig,
|
||||
SidebarOptions,
|
||||
SubmenuOptions,
|
||||
} from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { SidebarPinStateContext, useContent } from './Page';
|
||||
import { useContent } from './Page';
|
||||
import { SidebarOpenStateProvider } from './SidebarOpenStateContext';
|
||||
import { useSidebarPinState } from './SidebarPinStateContext';
|
||||
import { MobileSidebar } from './MobileSidebar';
|
||||
|
||||
/** @public */
|
||||
@@ -133,9 +134,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
|
||||
);
|
||||
const [state, setState] = useState(State.Closed);
|
||||
const hoverTimerRef = useRef<number>();
|
||||
const { isPinned, toggleSidebarPinState } = useContext(
|
||||
SidebarPinStateContext,
|
||||
);
|
||||
const { isPinned, toggleSidebarPinState } = useSidebarPinState();
|
||||
|
||||
const handleOpen = () => {
|
||||
if (isPinned || disableExpandOnHover) {
|
||||
@@ -191,7 +190,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
|
||||
return (
|
||||
<nav style={{}} aria-label="sidebar nav">
|
||||
<A11ySkipSidebar />
|
||||
<SidebarContext.Provider value={{ isOpen, setOpen }}>
|
||||
<SidebarOpenStateProvider value={{ isOpen, setOpen }}>
|
||||
<div
|
||||
className={classes.root}
|
||||
data-testid="sidebar-root"
|
||||
@@ -208,7 +207,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
</SidebarOpenStateProvider>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -226,7 +225,7 @@ export const Sidebar = (props: SidebarProps) => {
|
||||
props.submenuOptions ?? {},
|
||||
);
|
||||
const { children, disableExpandOnHover, openDelayMs, closeDelayMs } = props;
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
|
||||
return isMobile ? (
|
||||
<MobileSidebar>{children}</MobileSidebar>
|
||||
|
||||
@@ -25,10 +25,10 @@ import { useLocalStorageValue } from '@react-hookz/web';
|
||||
import {
|
||||
SidebarConfigContext,
|
||||
SidebarConfig,
|
||||
SidebarContext,
|
||||
SIDEBAR_INTRO_LOCAL_STORAGE,
|
||||
} from './config';
|
||||
import { SidebarDivider } from './Items';
|
||||
import { useSidebarOpenState } from './SidebarOpenStateContext';
|
||||
|
||||
/** @public */
|
||||
export type SidebarIntroClassKey =
|
||||
@@ -151,7 +151,7 @@ const recentlyViewedIntroText =
|
||||
'And your recently viewed plugins will pop up here!';
|
||||
|
||||
export function SidebarIntro(_props: {}) {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
const defaultValue = {
|
||||
starredItemsDismissed: false,
|
||||
recentlyViewedItemsDismissed: false,
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
useResolvedPath,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
SidebarContext,
|
||||
SidebarConfigContext,
|
||||
SidebarItemWithSubmenuContext,
|
||||
SidebarConfig,
|
||||
@@ -62,6 +61,7 @@ import DoubleArrowLeft from './icons/DoubleArrowLeft';
|
||||
import DoubleArrowRight from './icons/DoubleArrowRight';
|
||||
import { isLocationMatch } from './utils';
|
||||
import { Location } from 'history';
|
||||
import { useSidebarOpenState } from './SidebarOpenStateContext';
|
||||
|
||||
/** @public */
|
||||
export type SidebarItemClassKey =
|
||||
@@ -369,7 +369,7 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
|
||||
// depend on the current location, and at least have it being optionally forced to selected.
|
||||
// Still waiting on a Q answered to fine tune the implementation
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
|
||||
const divStyle =
|
||||
!isOpen && hasSubmenu ? { display: 'flex', marginLeft: '24px' } : {};
|
||||
@@ -671,7 +671,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => {
|
||||
export const SidebarExpandButton = () => {
|
||||
const { sidebarConfig } = useContext(SidebarConfigContext);
|
||||
const classes = useMemoStyles(sidebarConfig);
|
||||
const { isOpen, setOpen } = useContext(SidebarContext);
|
||||
const { isOpen, setOpen } = useSidebarOpenState();
|
||||
const isSmallScreen = useMediaQuery<BackstageTheme>(
|
||||
theme => theme.breakpoints.down('md'),
|
||||
{ noSsr: true },
|
||||
|
||||
@@ -27,8 +27,9 @@ import MenuIcon from '@material-ui/icons/Menu';
|
||||
import { orderBy } from 'lodash';
|
||||
import React, { createContext, useEffect, useState, useContext } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { SidebarOpenStateProvider } from './SidebarOpenStateContext';
|
||||
import { SidebarGroup } from './SidebarGroup';
|
||||
import { SidebarConfigContext, SidebarContext, SidebarConfig } from './config';
|
||||
import { SidebarConfigContext, SidebarConfig } from './config';
|
||||
|
||||
/**
|
||||
* Type of `MobileSidebarContext`
|
||||
@@ -207,7 +208,7 @@ export const MobileSidebar = (props: MobileSidebarProps) => {
|
||||
!sidebarGroups[selectedMenuItemIndex].props.to;
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={{ isOpen: true, setOpen: () => {} }}>
|
||||
<SidebarOpenStateProvider value={{ isOpen: true, setOpen: () => {} }}>
|
||||
<MobileSidebarContext.Provider
|
||||
value={{ selectedMenuItemIndex, setSelectedMenuItemIndex }}
|
||||
>
|
||||
@@ -231,6 +232,6 @@ export const MobileSidebar = (props: MobileSidebarProps) => {
|
||||
{sidebarGroups}
|
||||
</BottomNavigation>
|
||||
</MobileSidebarContext.Provider>
|
||||
</SidebarContext.Provider>
|
||||
</SidebarOpenStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SidebarConfigContext, SidebarConfig } from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { LocalStorage } from './localStorage';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import { SidebarPinStateProvider } from './SidebarPinStateContext';
|
||||
|
||||
export type SidebarPageClassKey = 'root';
|
||||
|
||||
@@ -62,17 +63,6 @@ const useStyles = makeStyles<
|
||||
{ name: 'BackstageSidebarPage' },
|
||||
);
|
||||
|
||||
/**
|
||||
* Type of `SidebarPinStateContext`
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarPinStateContextType = {
|
||||
isPinned: boolean;
|
||||
toggleSidebarPinState: () => any;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for SidebarPage
|
||||
*
|
||||
@@ -82,19 +72,6 @@ export type SidebarPageProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains the state on how the `Sidebar` is rendered
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SidebarPinStateContext = createContext<SidebarPinStateContextType>(
|
||||
{
|
||||
isPinned: true,
|
||||
toggleSidebarPinState: () => {},
|
||||
isMobile: false,
|
||||
},
|
||||
);
|
||||
|
||||
type PageContextType = {
|
||||
content: {
|
||||
contentRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
@@ -137,7 +114,7 @@ export function SidebarPage(props: SidebarPageProps) {
|
||||
const classes = useStyles({ isPinned, sidebarConfig });
|
||||
|
||||
return (
|
||||
<SidebarPinStateContext.Provider
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned,
|
||||
toggleSidebarPinState,
|
||||
@@ -147,7 +124,7 @@ export function SidebarPage(props: SidebarPageProps) {
|
||||
<PageContext.Provider value={pageContext}>
|
||||
<div className={classes.root}>{props.children}</div>
|
||||
</PageContext.Provider>
|
||||
</SidebarPinStateContext.Provider>
|
||||
</SidebarPinStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import BottomNavigationAction, {
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SidebarPinStateContext } from '.';
|
||||
import { useSidebarPinState } from '.';
|
||||
import { Link } from '../../components';
|
||||
import { SidebarConfigContext, SidebarConfig } from './config';
|
||||
import { MobileSidebarContext } from './MobileSidebar';
|
||||
@@ -122,7 +122,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => {
|
||||
*/
|
||||
export const SidebarGroup = (props: SidebarGroupProps) => {
|
||||
const { children, to, label, icon, value } = props;
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
|
||||
return isMobile ? (
|
||||
<MobileSidebarGroup to={to} label={label} icon={icon} value={value} />
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 React, { ReactNode, useContext } from 'react';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import {
|
||||
LegacySidebarContext,
|
||||
SidebarOpenStateProvider,
|
||||
useSidebarOpenState,
|
||||
} from './SidebarOpenStateContext';
|
||||
|
||||
describe('SidebarOpenStateContext', () => {
|
||||
describe('SidebarOpenStateProvider', () => {
|
||||
it('should render children', async () => {
|
||||
const { findByText } = await renderWithEffects(
|
||||
<SidebarOpenStateProvider value={{ isOpen: false, setOpen: () => {} }}>
|
||||
Child
|
||||
</SidebarOpenStateProvider>,
|
||||
);
|
||||
expect(await findByText('Child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should provide the legacy context as well, for now', async () => {
|
||||
const LegacyContextSpy = () => {
|
||||
const { isOpen } = useContext(LegacySidebarContext);
|
||||
return <>{String(isOpen)}</>;
|
||||
};
|
||||
|
||||
const { findByText } = await renderWithEffects(
|
||||
<SidebarOpenStateProvider
|
||||
value={{
|
||||
isOpen: true,
|
||||
setOpen: () => {},
|
||||
}}
|
||||
>
|
||||
<LegacyContextSpy />
|
||||
</SidebarOpenStateProvider>,
|
||||
);
|
||||
|
||||
expect(await findByText('true')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSidebarOpenState', () => {
|
||||
it('can be invoked within legacy context', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<SidebarOpenStateProvider
|
||||
value={{
|
||||
isOpen: true,
|
||||
setOpen: () => {},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarOpenStateProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSidebarOpenState(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isOpen).toBe(true);
|
||||
expect(typeof result.current.setOpen).toBe('function');
|
||||
});
|
||||
|
||||
it('does not need to be invoked within provider', () => {
|
||||
const { result } = renderHook(() => useSidebarOpenState());
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(typeof result.current.setOpen).toBe('function');
|
||||
});
|
||||
|
||||
it('should read and update state', async () => {
|
||||
let actualValue = true;
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<SidebarOpenStateProvider
|
||||
value={{
|
||||
isOpen: actualValue,
|
||||
setOpen: value => {
|
||||
actualValue = value;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarOpenStateProvider>
|
||||
);
|
||||
const { result, rerender } = renderHook(() => useSidebarOpenState(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isOpen).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.setOpen(false);
|
||||
rerender();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 React, { createContext, ReactNode, useContext } from 'react';
|
||||
import {
|
||||
createVersionedContext,
|
||||
createVersionedValueMap,
|
||||
} from '@backstage/version-bridge';
|
||||
|
||||
/**
|
||||
* Types for the `SidebarContext`
|
||||
*
|
||||
* @public @deprecated
|
||||
* Use `SidebarOpenState` instead.
|
||||
*/
|
||||
export type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The open state of the sidebar.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarOpenState = {
|
||||
/**
|
||||
* Whether or not the sidebar is open and full-width. When `false`, the
|
||||
* sidebar is "closed" and typically only shows icons with no text.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* A function to set whether or not the sidebar is open. Pass `true` to open
|
||||
* the sidebar. Pass `false` to close it.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
const defaultSidebarOpenStateContext = {
|
||||
isOpen: false,
|
||||
setOpen: () => {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Context whether the `Sidebar` is open
|
||||
*
|
||||
* @public @deprecated
|
||||
* Use `<SidebarContextProvider>` + `useSidebar()` instead.
|
||||
*/
|
||||
export const LegacySidebarContext = createContext<SidebarContextType>(
|
||||
defaultSidebarOpenStateContext,
|
||||
);
|
||||
|
||||
const VersionedSidebarContext = createVersionedContext<{
|
||||
1: SidebarOpenState;
|
||||
}>('sidebar-open-state-context');
|
||||
|
||||
/**
|
||||
* Provides context for reading and updating sidebar state.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SidebarOpenStateProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
value: SidebarOpenState;
|
||||
}) => (
|
||||
<LegacySidebarContext.Provider value={value}>
|
||||
<VersionedSidebarContext.Provider
|
||||
value={createVersionedValueMap({ 1: value })}
|
||||
>
|
||||
{children}
|
||||
</VersionedSidebarContext.Provider>
|
||||
</LegacySidebarContext.Provider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to read and update the sidebar's open state, which controls whether or
|
||||
* not the sidebar is open and full-width, or closed and only displaying icons.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const useSidebarOpenState = (): SidebarOpenState => {
|
||||
const versionedOpenStateContext = useContext(VersionedSidebarContext);
|
||||
const legacyOpenStateContext = useContext(LegacySidebarContext);
|
||||
|
||||
// Invoked from outside a SidebarOpenStateProvider: check for the legacy
|
||||
// context's value, but otherwise return the default.
|
||||
if (versionedOpenStateContext === undefined) {
|
||||
return legacyOpenStateContext || defaultSidebarOpenStateContext;
|
||||
}
|
||||
|
||||
const openStateContext = versionedOpenStateContext.atVersion(1);
|
||||
if (openStateContext === undefined) {
|
||||
throw new Error('No context found for version 1.');
|
||||
}
|
||||
|
||||
return openStateContext;
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 React, { ReactNode, useContext } from 'react';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import {
|
||||
LegacySidebarPinStateContext,
|
||||
SidebarPinStateProvider,
|
||||
useSidebarPinState,
|
||||
} from './SidebarPinStateContext';
|
||||
|
||||
describe('SidebarPinStateContext', () => {
|
||||
describe('SidebarPinStateProvider', () => {
|
||||
it('should render children', async () => {
|
||||
const { findByText } = await renderWithEffects(
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned: true,
|
||||
isMobile: false,
|
||||
toggleSidebarPinState: () => {},
|
||||
}}
|
||||
>
|
||||
Child
|
||||
</SidebarPinStateProvider>,
|
||||
);
|
||||
expect(await findByText('Child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should provide the legacy context as well, for now', async () => {
|
||||
const LegacyContextSpy = () => {
|
||||
const { isMobile } = useContext(LegacySidebarPinStateContext);
|
||||
return <>{String(isMobile)}</>;
|
||||
};
|
||||
|
||||
const { findByText } = await renderWithEffects(
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned: true,
|
||||
isMobile: true,
|
||||
toggleSidebarPinState: () => {},
|
||||
}}
|
||||
>
|
||||
<LegacyContextSpy />
|
||||
</SidebarPinStateProvider>,
|
||||
);
|
||||
|
||||
expect(await findByText('true')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSidebarPinState', () => {
|
||||
it('can be invoked within legacy context', () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<LegacySidebarPinStateContext.Provider
|
||||
value={{
|
||||
isPinned: true,
|
||||
isMobile: true,
|
||||
toggleSidebarPinState: () => {},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LegacySidebarPinStateContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSidebarPinState(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isPinned).toBe(true);
|
||||
expect(result.current.isMobile).toBe(true);
|
||||
expect(typeof result.current.toggleSidebarPinState).toBe('function');
|
||||
});
|
||||
|
||||
it('does not need to be invoked within provider', () => {
|
||||
const { result } = renderHook(() => useSidebarPinState());
|
||||
expect(result.current.isPinned).toBe(true);
|
||||
expect(result.current.isMobile).toBe(false);
|
||||
expect(typeof result.current.toggleSidebarPinState).toBe('function');
|
||||
});
|
||||
|
||||
it('should read and update state', async () => {
|
||||
let actualValue = true;
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned: actualValue,
|
||||
isMobile: false,
|
||||
toggleSidebarPinState: () => {
|
||||
actualValue = !actualValue;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarPinStateProvider>
|
||||
);
|
||||
const { result, rerender } = renderHook(() => useSidebarPinState(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.isPinned).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleSidebarPinState();
|
||||
rerender();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isPinned).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 {
|
||||
createVersionedContext,
|
||||
createVersionedValueMap,
|
||||
} from '@backstage/version-bridge';
|
||||
import React, { createContext, ReactNode, useContext } from 'react';
|
||||
|
||||
/**
|
||||
* Type of `SidebarPinStateContext`
|
||||
*
|
||||
* @public @deprecated
|
||||
* Use `SidebarPinState` instead.
|
||||
*/
|
||||
export type SidebarPinStateContextType = {
|
||||
isPinned: boolean;
|
||||
toggleSidebarPinState: () => any;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The pin state of the sidebar.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarPinState = {
|
||||
/**
|
||||
* Whether or not the sidebar is pinned to the `open` state. When `isPinned`
|
||||
* is `false`, the sidebar opens and closes on hover. When `true`, the
|
||||
* sidebar is permanently opened, regardless of user interaction.
|
||||
*/
|
||||
isPinned: boolean;
|
||||
|
||||
/**
|
||||
* A function to toggle the pin state of the sidebar.
|
||||
*/
|
||||
toggleSidebarPinState: () => any;
|
||||
|
||||
/**
|
||||
* Whether or not the sidebar is or should be rendered in a mobile-optimized
|
||||
* way.
|
||||
*/
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const defaultSidebarPinStateContext = {
|
||||
isPinned: true,
|
||||
toggleSidebarPinState: () => {},
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains the state on how the `Sidebar` is rendered
|
||||
*
|
||||
* @public @deprecated
|
||||
* Use `<SidebarPinStateContextProvider>` + `useSidebarPinState()` instead.
|
||||
*/
|
||||
export const LegacySidebarPinStateContext =
|
||||
createContext<SidebarPinStateContextType>(defaultSidebarPinStateContext);
|
||||
|
||||
const VersionedSidebarPinStateContext = createVersionedContext<{
|
||||
1: SidebarPinState;
|
||||
}>('sidebar-pin-state-context');
|
||||
|
||||
/**
|
||||
* Provides state for how the `Sidebar` is rendered
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SidebarPinStateProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
value: SidebarPinStateContextType;
|
||||
}) => (
|
||||
<LegacySidebarPinStateContext.Provider value={value}>
|
||||
<VersionedSidebarPinStateContext.Provider
|
||||
value={createVersionedValueMap({ 1: value })}
|
||||
>
|
||||
{children}
|
||||
</VersionedSidebarPinStateContext.Provider>
|
||||
</LegacySidebarPinStateContext.Provider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to read and update sidebar pin state, which controls whether or not the
|
||||
* sidebar is pinned open.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const useSidebarPinState = (): SidebarPinState => {
|
||||
const versionedPinStateContext = useContext(VersionedSidebarPinStateContext);
|
||||
const legacyPinStateContext = useContext(LegacySidebarPinStateContext);
|
||||
|
||||
// Invoked from outside a SidebarPinStateProvider: check for the legacy
|
||||
// context's value, but otherwise return the default.
|
||||
if (versionedPinStateContext === undefined) {
|
||||
return legacyPinStateContext || defaultSidebarPinStateContext;
|
||||
}
|
||||
|
||||
const pinStateContext = versionedPinStateContext.atVersion(1);
|
||||
if (pinStateContext === undefined) {
|
||||
throw new Error('No context found for version 1.');
|
||||
}
|
||||
|
||||
return pinStateContext;
|
||||
};
|
||||
@@ -19,10 +19,10 @@ import classnames from 'classnames';
|
||||
import React, { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
SidebarItemWithSubmenuContext,
|
||||
SidebarContext,
|
||||
SidebarConfigContext,
|
||||
SubmenuConfig,
|
||||
} from './config';
|
||||
import { useSidebarOpenState } from './SidebarOpenStateContext';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<
|
||||
@@ -105,7 +105,7 @@ export type SidebarSubmenuProps = {
|
||||
* @public
|
||||
*/
|
||||
export const SidebarSubmenu = (props: SidebarSubmenuProps) => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
const { sidebarConfig, submenuConfig } = useContext(SidebarConfigContext);
|
||||
const left = isOpen
|
||||
? sidebarConfig.drawerWidthOpen
|
||||
|
||||
@@ -101,22 +101,6 @@ export const makeSidebarSubmenuConfig = (
|
||||
export const SIDEBAR_INTRO_LOCAL_STORAGE =
|
||||
'@backstage/core/sidebar-intro-dismissed';
|
||||
|
||||
/**
|
||||
* Types for the `SidebarContext`
|
||||
*/
|
||||
export type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context whether the `Sidebar` is open
|
||||
*/
|
||||
export const SidebarContext = createContext<SidebarContextType>({
|
||||
isOpen: false,
|
||||
setOpen: () => {},
|
||||
});
|
||||
|
||||
export type SidebarConfigContextType = {
|
||||
sidebarConfig: SidebarConfig;
|
||||
submenuConfig: SubmenuConfig;
|
||||
|
||||
@@ -27,16 +27,8 @@ export type {
|
||||
SidebarSubmenuItemDropdownItem,
|
||||
} from './SidebarSubmenuItem';
|
||||
export type { SidebarClassKey, SidebarProps } from './Bar';
|
||||
export {
|
||||
SidebarPage,
|
||||
SidebarPinStateContext as SidebarPinStateContext,
|
||||
useContent,
|
||||
} from './Page';
|
||||
export type {
|
||||
SidebarPinStateContextType as SidebarPinStateContextType,
|
||||
SidebarPageClassKey,
|
||||
SidebarPageProps,
|
||||
} from './Page';
|
||||
export { SidebarPage, useContent } from './Page';
|
||||
export type { SidebarPageClassKey, SidebarPageProps } from './Page';
|
||||
export {
|
||||
SidebarDivider,
|
||||
SidebarItem,
|
||||
@@ -54,13 +46,23 @@ export type {
|
||||
} from './Items';
|
||||
export { IntroCard, SidebarIntro } from './Intro';
|
||||
export type { SidebarIntroClassKey } from './Intro';
|
||||
export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config';
|
||||
export type { SidebarOptions, SubmenuOptions } from './config';
|
||||
export {
|
||||
SIDEBAR_INTRO_LOCAL_STORAGE,
|
||||
SidebarContext,
|
||||
sidebarConfig,
|
||||
} from './config';
|
||||
LegacySidebarContext as SidebarContext,
|
||||
SidebarOpenStateProvider,
|
||||
useSidebarOpenState,
|
||||
} from './SidebarOpenStateContext';
|
||||
export type {
|
||||
SidebarContextType,
|
||||
SidebarOptions,
|
||||
SubmenuOptions,
|
||||
} from './config';
|
||||
SidebarOpenState,
|
||||
} from './SidebarOpenStateContext';
|
||||
export {
|
||||
LegacySidebarPinStateContext as SidebarPinStateContext,
|
||||
SidebarPinStateProvider,
|
||||
useSidebarPinState,
|
||||
} from './SidebarPinStateContext';
|
||||
export type {
|
||||
SidebarPinStateContextType,
|
||||
SidebarPinState,
|
||||
} from './SidebarPinStateContext';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, PropsWithChildren } from 'react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Link, makeStyles } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
@@ -32,13 +32,13 @@ import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
} from '@backstage/core-components';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
@@ -60,7 +60,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
|
||||
const SidebarLogo = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren, useContext } from 'react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
import { Link, makeStyles } from '@material-ui/core';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
SidebarPage,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarContext,
|
||||
useSidebarOpenState,
|
||||
} from '@backstage/core-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
@@ -48,7 +48,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
|
||||
const SidebarLogo = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
|
||||
import { DocumentDecoratorFactory } from '@backstage/plugin-search-common';
|
||||
import { DocumentTypeInfo } from '@backstage/plugin-search-common';
|
||||
@@ -12,6 +13,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { IndexableResultSet } from '@backstage/plugin-search-common';
|
||||
import { Logger } from 'winston';
|
||||
import { default as lunr_2 } from 'lunr';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { QueryTranslator } from '@backstage/plugin-search-common';
|
||||
import { Readable } from 'stream';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
@@ -19,6 +21,7 @@ import { SearchQuery } from '@backstage/plugin-search-common';
|
||||
import { TaskFunction } from '@backstage/backend-tasks';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { Transform } from 'stream';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// @beta
|
||||
@@ -112,6 +115,31 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// @beta
|
||||
export class NewlineDelimitedJsonCollatorFactory
|
||||
implements DocumentCollatorFactory
|
||||
{
|
||||
static fromConfig(
|
||||
_config: Config,
|
||||
options: NewlineDelimitedJsonCollatorFactoryOptions,
|
||||
): NewlineDelimitedJsonCollatorFactory;
|
||||
// (undocumented)
|
||||
getCollator(): Promise<Readable>;
|
||||
// (undocumented)
|
||||
readonly type: string;
|
||||
// (undocumented)
|
||||
readonly visibilityPermission: Permission | undefined;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
type: string;
|
||||
searchPattern: string;
|
||||
reader: UrlReader;
|
||||
logger: Logger;
|
||||
visibilityPermission?: Permission;
|
||||
};
|
||||
|
||||
// @beta
|
||||
export interface RegisterCollatorParameters {
|
||||
factory: DocumentCollatorFactory;
|
||||
|
||||
@@ -23,19 +23,24 @@
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.13.6-next.0",
|
||||
"@backstage/backend-tasks": "^0.3.2-next.0",
|
||||
"@backstage/config": "^1.0.1",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/plugin-permission-common": "^0.6.1",
|
||||
"@backstage/plugin-search-common": "^0.3.4",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lunr": "^2.3.9",
|
||||
"ndjson": "^2.0.0",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.13.6-next.0",
|
||||
"@backstage/cli": "^0.17.2-next.0"
|
||||
"@backstage/cli": "^0.17.2-next.0",
|
||||
"@types/ndjson": "^2.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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 {
|
||||
getVoidLogger,
|
||||
ReadUrlResponse,
|
||||
UrlReader,
|
||||
UrlReaders,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Readable } from 'stream';
|
||||
import { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory';
|
||||
import { TestPipeline } from '../test-utils';
|
||||
|
||||
describe('DefaultCatalogCollatorFactory', () => {
|
||||
const config = new ConfigReader({});
|
||||
const logger = getVoidLogger();
|
||||
|
||||
it('has expected type', () => {
|
||||
const factory = NewlineDelimitedJsonCollatorFactory.fromConfig(config, {
|
||||
type: 'expected-type',
|
||||
searchPattern: 'test://folder/prefix-*',
|
||||
logger,
|
||||
reader: UrlReaders.default({ logger, config }),
|
||||
});
|
||||
expect(factory.type).toBe('expected-type');
|
||||
});
|
||||
|
||||
describe('getCollator', () => {
|
||||
let readable: Readable;
|
||||
let reader: jest.Mocked<
|
||||
UrlReader & { readUrl: jest.Mock<Promise<ReadUrlResponse>> }
|
||||
>;
|
||||
let factory: NewlineDelimitedJsonCollatorFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
readable = new Readable();
|
||||
readable._read = () => {};
|
||||
reader = {
|
||||
search: jest.fn(),
|
||||
read: jest.fn(),
|
||||
readTree: jest.fn(),
|
||||
readUrl: jest.fn(),
|
||||
};
|
||||
factory = NewlineDelimitedJsonCollatorFactory.fromConfig(config, {
|
||||
type: 'expected-type',
|
||||
searchPattern: 'test://folder/prefix-*',
|
||||
logger,
|
||||
reader: UrlReaders.create({
|
||||
logger,
|
||||
config,
|
||||
factories: [() => [{ predicate: () => true, reader }]],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if url reader throws an error during search', async () => {
|
||||
reader.search.mockRejectedValue(new Error('Expected error'));
|
||||
|
||||
await expect(() => factory.getCollator()).rejects.toThrowError(
|
||||
'Expected error',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if no matching files are found', async () => {
|
||||
reader.search.mockResolvedValue({ files: [], etag: '' });
|
||||
|
||||
await expect(() => factory.getCollator()).rejects.toThrowError(
|
||||
'Could not find an .ndjson file matching',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if matching file is not .ndjson', async () => {
|
||||
reader.search.mockResolvedValue({
|
||||
files: [{ url: 'test://folder/prefix-1.avro', content: jest.fn() }],
|
||||
etag: '',
|
||||
});
|
||||
reader.readUrl.mockResolvedValue({
|
||||
buffer: jest.fn(),
|
||||
stream: jest.fn().mockReturnValue(readable),
|
||||
});
|
||||
|
||||
await expect(() => factory.getCollator()).rejects.toThrowError(
|
||||
'Could not find an .ndjson file matching',
|
||||
);
|
||||
});
|
||||
|
||||
it('gets stream using latest matched url', async () => {
|
||||
reader.search.mockResolvedValue({
|
||||
files: [
|
||||
{ url: 'test://folder/prefix-1.ndjson', content: jest.fn() },
|
||||
{ url: 'test://folder/prefix-2.ndjson', content: jest.fn() },
|
||||
],
|
||||
etag: '',
|
||||
});
|
||||
reader.readUrl.mockResolvedValue({
|
||||
buffer: jest.fn(),
|
||||
stream: jest.fn().mockReturnValue(readable),
|
||||
});
|
||||
|
||||
await factory.getCollator();
|
||||
|
||||
expect(reader.search).toHaveBeenCalledWith(
|
||||
'test://folder/prefix-*',
|
||||
undefined,
|
||||
);
|
||||
expect(reader.readUrl).toHaveBeenCalledWith(
|
||||
'test://folder/prefix-2.ndjson',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('transforms newline delimited json into readable stream of documents', async () => {
|
||||
reader.search.mockResolvedValue({
|
||||
files: [{ url: 'test://folder/prefix-1.ndjson', content: jest.fn() }],
|
||||
etag: '',
|
||||
});
|
||||
reader.readUrl.mockResolvedValue({
|
||||
buffer: jest.fn(),
|
||||
stream: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Readable.from(
|
||||
'{"title": "Title 1", "location": "/title-1", "text": "text 1"}\n{"title": "Title 2", "location": "/title-2", "text": "text 2"}',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const collator = await factory.getCollator();
|
||||
const pipeline = TestPipeline.withSubject(collator);
|
||||
const { documents } = await pipeline.execute();
|
||||
|
||||
expect(documents).toHaveLength(2);
|
||||
expect(documents[0].title).toBe('Title 1');
|
||||
expect(documents[0].location).toBe('/title-1');
|
||||
expect(documents[0].text).toBe('text 1');
|
||||
expect(documents[1].title).toBe('Title 2');
|
||||
expect(documents[1].location).toBe('/title-2');
|
||||
expect(documents[1].text).toBe('text 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 { UrlReader } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
|
||||
import { parse as parseNdjson } from 'ndjson';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
*/
|
||||
export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
type: string;
|
||||
searchPattern: string;
|
||||
reader: UrlReader;
|
||||
logger: Logger;
|
||||
visibilityPermission?: Permission;
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory class producing a collator that can be used to index documents
|
||||
* sourced from the latest newline delimited JSON file matching a given search
|
||||
* pattern. "Latest" is determined by the name of the file (last alphabetically
|
||||
* is considered latest).
|
||||
*
|
||||
* @remarks
|
||||
* The reader provided must implement the `search()` method as well as the
|
||||
* `readUrl` method whose response includes the `stream()` method. Naturally,
|
||||
* the reader must also be configured to understand the given search pattern.
|
||||
*
|
||||
* @example
|
||||
* Here's an example configuration using Google Cloud Storage, which would
|
||||
* return the latest file under the `bucket` GCS bucket with files like
|
||||
* `xyz-2021.ndjson` or `xyz-2022.ndjson`.
|
||||
* ```ts
|
||||
* indexBuilder.addCollator({
|
||||
* schedule,
|
||||
* factory: NewlineDelimitedJsonCollatorFactory.fromConfig(env.config, {
|
||||
* type: 'techdocs',
|
||||
* searchPattern: 'https://storage.cloud.google.com/bucket/xyz-*',
|
||||
* reader: env.reader,
|
||||
* logger: env.logger,
|
||||
* })
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @beta
|
||||
*/
|
||||
export class NewlineDelimitedJsonCollatorFactory
|
||||
implements DocumentCollatorFactory
|
||||
{
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory."type"} */
|
||||
readonly type: string;
|
||||
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.visibilityPermission} */
|
||||
public readonly visibilityPermission: Permission | undefined;
|
||||
|
||||
private constructor(
|
||||
type: string,
|
||||
private readonly searchPattern: string,
|
||||
private readonly reader: UrlReader,
|
||||
private readonly logger: Logger,
|
||||
visibilityPermission: Permission | undefined,
|
||||
) {
|
||||
this.type = type;
|
||||
this.visibilityPermission = visibilityPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a NewlineDelimitedJsonCollatorFactory instance from configuration
|
||||
* and a set of options.
|
||||
*/
|
||||
static fromConfig(
|
||||
_config: Config,
|
||||
options: NewlineDelimitedJsonCollatorFactoryOptions,
|
||||
): NewlineDelimitedJsonCollatorFactory {
|
||||
return new NewlineDelimitedJsonCollatorFactory(
|
||||
options.type,
|
||||
options.searchPattern,
|
||||
options.reader,
|
||||
options.logger,
|
||||
options.visibilityPermission,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "latest" URL for the given search pattern (e.g. the one at the
|
||||
* end of the list, sorted alphabetically).
|
||||
*/
|
||||
private async lastUrl(): Promise<string | undefined> {
|
||||
try {
|
||||
// Search for files matching the given pattern, then sort/reverse. The
|
||||
// first item in the list will be the "latest" file.
|
||||
this.logger.info(
|
||||
`Attempting to find latest .ndjson matching ${this.searchPattern}`,
|
||||
);
|
||||
const { files } = await this.reader.search(this.searchPattern);
|
||||
const candidates = files
|
||||
.filter(file => file.url.endsWith('.ndjson'))
|
||||
.sort((a, b) => a.url.localeCompare(b.url))
|
||||
.reverse();
|
||||
|
||||
return candidates[0]?.url;
|
||||
} catch (e) {
|
||||
this.logger.error(`Could not search for ${this.searchPattern}`, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.getCollator} */
|
||||
async getCollator(): Promise<Readable> {
|
||||
// Search for files matching the given pattern.
|
||||
const lastUrl = await this.lastUrl();
|
||||
|
||||
// Abort if no such file could be found.
|
||||
if (!lastUrl) {
|
||||
const noMatchingFile = `Could not find an .ndjson file matching ${this.searchPattern}`;
|
||||
this.logger.error(noMatchingFile);
|
||||
throw new Error(noMatchingFile);
|
||||
} else {
|
||||
this.logger.info(`Using latest .ndjson file ${lastUrl}`);
|
||||
}
|
||||
|
||||
// Use the UrlReader to try and stream the file.
|
||||
const readerResponse = await this.reader.readUrl!(lastUrl);
|
||||
const stream = readerResponse.stream!();
|
||||
|
||||
// Use ndjson's parser to turn the raw file into an object-mode stream.
|
||||
return stream.pipe(parseNdjson());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { NewlineDelimitedJsonCollatorFactoryOptions } from './NewlineDelimitedJsonCollatorFactory';
|
||||
|
||||
export { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory';
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
export { IndexBuilder } from './IndexBuilder';
|
||||
export { Scheduler } from './Scheduler';
|
||||
export * from './collators';
|
||||
export { LunrSearchEngine } from './engines';
|
||||
export type {
|
||||
ConcreteLunrQuery,
|
||||
|
||||
@@ -66,14 +66,14 @@ export class TestPipeline {
|
||||
return new TestPipeline({ decorator: subject });
|
||||
}
|
||||
|
||||
if (subject instanceof Readable) {
|
||||
return new TestPipeline({ collator: subject });
|
||||
}
|
||||
|
||||
if (subject instanceof Writable) {
|
||||
return new TestPipeline({ indexer: subject });
|
||||
}
|
||||
|
||||
if (subject.readable || subject instanceof Readable) {
|
||||
return new TestPipeline({ collator: subject });
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Unknown test subject: are you passing a readable, writable, or transform stream?',
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ShortcutItem } from './ShortcutItem';
|
||||
import { Shortcut } from './types';
|
||||
import { LocalStoredShortcuts } from './api';
|
||||
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
|
||||
import { SidebarContext } from '@backstage/core-components';
|
||||
import { SidebarOpenStateProvider } from '@backstage/core-components';
|
||||
|
||||
describe('ShortcutItem', () => {
|
||||
const shortcut: Shortcut = {
|
||||
@@ -32,9 +32,9 @@ describe('ShortcutItem', () => {
|
||||
|
||||
it('displays the shortcut', async () => {
|
||||
await renderInTestApp(
|
||||
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<SidebarOpenStateProvider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<ShortcutItem api={api} shortcut={shortcut} />
|
||||
</SidebarContext.Provider>,
|
||||
</SidebarOpenStateProvider>,
|
||||
);
|
||||
expect(screen.getByText('ST')).toBeInTheDocument();
|
||||
expect(screen.getByText('some title')).toBeInTheDocument();
|
||||
|
||||
@@ -24,12 +24,12 @@ import { screen, waitFor } from '@testing-library/react';
|
||||
import { Shortcuts } from './Shortcuts';
|
||||
import { LocalStoredShortcuts, shortcutsApiRef } from './api';
|
||||
|
||||
import { SidebarContext } from '@backstage/core-components';
|
||||
import { SidebarOpenStateProvider } from '@backstage/core-components';
|
||||
|
||||
describe('Shortcuts', () => {
|
||||
it('displays an add button', async () => {
|
||||
await renderInTestApp(
|
||||
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<SidebarOpenStateProvider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
@@ -40,7 +40,7 @@ describe('Shortcuts', () => {
|
||||
>
|
||||
<Shortcuts />
|
||||
</TestApiProvider>
|
||||
</SidebarContext.Provider>,
|
||||
</SidebarOpenStateProvider>,
|
||||
);
|
||||
await waitFor(() => !screen.queryByTestId('progress'));
|
||||
expect(screen.getByText('Add Shortcuts')).toBeInTheDocument();
|
||||
|
||||
@@ -10,6 +10,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common';
|
||||
import { CheckResult } from '@backstage/plugin-tech-insights-common';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -78,6 +80,33 @@ export interface TechInsightsApi {
|
||||
// @public
|
||||
export const techInsightsApiRef: ApiRef<TechInsightsApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class TechInsightsClient implements TechInsightsApi {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
});
|
||||
// (undocumented)
|
||||
getAllChecks(): Promise<Check[]>;
|
||||
// (undocumented)
|
||||
getScorecardsDefinition(
|
||||
type: string,
|
||||
value: CheckResult[],
|
||||
title?: string,
|
||||
description?: string,
|
||||
): CheckResultRenderer | undefined;
|
||||
// (undocumented)
|
||||
runBulkChecks(
|
||||
entities: CompoundEntityRef[],
|
||||
checks?: Check[],
|
||||
): Promise<BulkCheckResponse>;
|
||||
// (undocumented)
|
||||
runChecks(
|
||||
entityParams: CompoundEntityRef,
|
||||
checks?: string[],
|
||||
): Promise<CheckResult[]>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const techInsightsPlugin: BackstagePlugin<
|
||||
{
|
||||
|
||||
@@ -29,16 +29,15 @@ import {
|
||||
defaultCheckResultRenderers,
|
||||
} from '../components/CheckResultRenderer';
|
||||
|
||||
export type Options = {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export class TechInsightsClient implements TechInsightsApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: Options) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
export * from './TechInsightsApi';
|
||||
export * from './TechInsightsClient';
|
||||
export * from './types';
|
||||
|
||||
@@ -19,7 +19,6 @@ export {
|
||||
EntityTechInsightsScorecardCard,
|
||||
} from './plugin';
|
||||
|
||||
export { techInsightsApiRef } from './api/TechInsightsApi';
|
||||
export type { TechInsightsApi } from './api/TechInsightsApi';
|
||||
export type { Check } from './api/types';
|
||||
export { techInsightsApiRef, TechInsightsClient } from './api';
|
||||
export type { TechInsightsApi, Check } from './api';
|
||||
export type { CheckResultRenderer } from './components/CheckResultRenderer';
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useTheme } from '@material-ui/core';
|
||||
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { useSidebarPinState } from '@backstage/core-components';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
import { Transformer } from '../transformer';
|
||||
@@ -27,7 +27,7 @@ import { rules } from './rules';
|
||||
/**
|
||||
* Sidebar pinned state to be used in computing style injections.
|
||||
*/
|
||||
const useSidebar = () => useContext(SidebarPinStateContext);
|
||||
const useSidebar = () => useSidebarPinState();
|
||||
|
||||
/**
|
||||
* Process all rules and concatenate their definitions into a single style.
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { InfoCard, SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { InfoCard, useSidebarPinState } from '@backstage/core-components';
|
||||
import { List } from '@material-ui/core';
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import { UserSettingsPinToggle } from './UserSettingsPinToggle';
|
||||
import { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
|
||||
|
||||
export const UserSettingsAppearanceCard = () => {
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
|
||||
return (
|
||||
<InfoCard title="Appearance" variant="gridItem">
|
||||
|
||||
@@ -18,14 +18,14 @@ import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { UserSettingsPinToggle } from './UserSettingsPinToggle';
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { SidebarPinStateProvider } from '@backstage/core-components';
|
||||
|
||||
describe('<UserSettingsPinToggle />', () => {
|
||||
it('toggles the pin sidebar button', async () => {
|
||||
const mockToggleFn = jest.fn();
|
||||
const rendered = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<SidebarPinStateContext.Provider
|
||||
<SidebarPinStateProvider
|
||||
value={{
|
||||
isPinned: false,
|
||||
isMobile: false,
|
||||
@@ -33,7 +33,7 @@ describe('<UserSettingsPinToggle />', () => {
|
||||
}}
|
||||
>
|
||||
<UserSettingsPinToggle />
|
||||
</SidebarPinStateContext.Provider>,
|
||||
</SidebarPinStateProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
@@ -22,12 +22,10 @@ import {
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { useSidebarPinState } from '@backstage/core-components';
|
||||
|
||||
export const UserSettingsPinToggle = () => {
|
||||
const { isPinned, toggleSidebarPinState } = useContext(
|
||||
SidebarPinStateContext,
|
||||
);
|
||||
const { isPinned, toggleSidebarPinState } = useSidebarPinState();
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
import {
|
||||
Header,
|
||||
Page,
|
||||
SidebarPinStateContext,
|
||||
TabbedLayout,
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
import { useOutlet } from 'react-router';
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import { UserSettingsAuthProviders } from './AuthProviders';
|
||||
@@ -33,7 +33,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SettingsPage = ({ providerSettings }: Props) => {
|
||||
const { isMobile } = useContext(SidebarPinStateContext);
|
||||
const { isMobile } = useSidebarPinState();
|
||||
const outlet = useOutlet();
|
||||
|
||||
const tabs = useElementFilter(outlet, elements =>
|
||||
|
||||
@@ -4908,9 +4908,9 @@
|
||||
integrity sha512-8UiDeDbjCImFSfOegGu13otQ7OdP9FOYpcLjeouppnhs+MPeIEAtYS+jCcBKmi3reyTagC15/KVSRhde1wS1vg==
|
||||
|
||||
"@roadiehq/backstage-plugin-buildkite@^2.0.0":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-2.0.3.tgz#73f3586c176c4c2ffe55af78ee8ce86c3c363dc6"
|
||||
integrity sha512-Jna1m/pj52G7qz0PKmldQUXHsw5IUTxtgxVh2n1X2CTq+o10xySVspmEyEmLdNxkeQdYO+a280fYWjEKo9xoyQ==
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-2.0.4.tgz#cd72fd35a9f8b6bb1c2ccd3c13d2aece2ecaf86e"
|
||||
integrity sha512-bOrqKO9MmRB5jgue+S8WEmJk83h6g4EdGp2qmirVODQddL03gtFNgMTauQkKAV7T8N+C948xDcgdL3pjvLYJdA==
|
||||
dependencies:
|
||||
"@backstage/catalog-model" "^1.0.0"
|
||||
"@backstage/core-components" "^0.9.0"
|
||||
@@ -6224,6 +6224,14 @@
|
||||
resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||
|
||||
"@types/ndjson@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/ndjson/-/ndjson-2.0.1.tgz#0279bc20949bfb861d69ac3de5292775b169a2d0"
|
||||
integrity sha512-xSRLa/CtPjEo0plSQj+nMKjVBkYh5MeMwOXa1y//jFELdmy9AmVQgWKWQgZ+/XrNlAYxXtmKR8OHaizPgEpUEw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/through" "*"
|
||||
|
||||
"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
|
||||
@@ -7201,9 +7209,9 @@
|
||||
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
||||
|
||||
"@yarnpkg/parsers@^3.0.0-rc.4":
|
||||
version "3.0.0-rc.4"
|
||||
resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.4.tgz#d7b19fa22ce7ff2423e5cbf008f7be0a85e9f1e3"
|
||||
integrity sha512-ScXXCUwGdx+aEIP20U8VEGtuXmxcMPFdJTb9G9a8MRpQPxIkky/GYXEL5Hf4oqJJXGyCv/DN31zfmxyj31SLKw==
|
||||
version "3.0.0-rc.6"
|
||||
resolved "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.6.tgz#3c93267fdae4470e4eaaf8c5f81d0d00ea177f76"
|
||||
integrity sha512-YqtJ9VQqQixZsJJS4X83e6RMpgK1jmQJSIrCfd1wO3i/7vPk9QoLvvZS4bwZ2ha8QWqWlO/alAcXCGBezEI1Ig==
|
||||
dependencies:
|
||||
js-yaml "^3.10.0"
|
||||
tslib "^1.13.0"
|
||||
@@ -9492,6 +9500,11 @@ commander@^9.1.0:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9"
|
||||
integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==
|
||||
|
||||
commander@^9.3.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz#f619114a5a2d2054e0d9ff1b31d5ccf89255e26b"
|
||||
integrity sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==
|
||||
|
||||
common-ancestor-path@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7"
|
||||
@@ -9880,9 +9893,9 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10:
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.4.1, core-js@^3.6.5:
|
||||
version "3.22.5"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz#a5f5a58e663d5c0ebb4e680cd7be37536fb2a9cf"
|
||||
integrity sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA==
|
||||
version "3.22.7"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz#8d6c37f630f6139b8732d10f2c114c3f1d00024f"
|
||||
integrity sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
@@ -14075,9 +14088,9 @@ human-signals@^2.1.0:
|
||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||
|
||||
humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0, humanize-duration@^3.27.1:
|
||||
version "3.27.1"
|
||||
resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69"
|
||||
integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA==
|
||||
version "3.27.2"
|
||||
resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.2.tgz#4b4e565bec098d22c9a54344e16156d1c649f160"
|
||||
integrity sha512-A15OmA3FLFRnehvF4ZMocsxTZYvHq4ze7L+AgR1DeHw0xC9vMd4euInY83uqGU9/XXKNnVIEeKc1R8G8nKqtzg==
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
version "1.2.1"
|
||||
@@ -15090,7 +15103,7 @@ isbinaryfile@^5.0.0:
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
isobject@^2.0.0:
|
||||
version "2.1.0"
|
||||
@@ -16499,7 +16512,12 @@ libnpmpublish@^4.0.0:
|
||||
semver "^7.1.3"
|
||||
ssri "^8.0.0"
|
||||
|
||||
lilconfig@2.0.4, lilconfig@^2.0.3:
|
||||
lilconfig@2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"
|
||||
integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
|
||||
|
||||
lilconfig@^2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082"
|
||||
integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==
|
||||
@@ -16517,23 +16535,23 @@ linkify-it@^3.0.1:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
lint-staged@^12.2.0:
|
||||
version "12.4.1"
|
||||
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.1.tgz#63fa27bfc8a33515f6902f63f6670864f1fb233c"
|
||||
integrity sha512-PTXgzpflrQ+pODQTG116QNB+Q6uUTDg5B5HqGvNhoQSGt8Qy+MA/6zSnR8n38+sxP5TapzeQGTvoKni0KRS8Vg==
|
||||
version "12.4.3"
|
||||
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.3.tgz#914fa468458364e14cc952145db552d87c8847b6"
|
||||
integrity sha512-eH6SKOmdm/ZwCRMTZAmM3q3dPkpq6vco/BfrOw8iGun4Xs/thYegPD/MLIwKO+iPkzibkLJuQcRhRLXKvaKreg==
|
||||
dependencies:
|
||||
cli-truncate "^3.1.0"
|
||||
colorette "^2.0.16"
|
||||
commander "^8.3.0"
|
||||
debug "^4.3.3"
|
||||
commander "^9.3.0"
|
||||
debug "^4.3.4"
|
||||
execa "^5.1.1"
|
||||
lilconfig "2.0.4"
|
||||
listr2 "^4.0.1"
|
||||
micromatch "^4.0.4"
|
||||
lilconfig "2.0.5"
|
||||
listr2 "^4.0.5"
|
||||
micromatch "^4.0.5"
|
||||
normalize-path "^3.0.0"
|
||||
object-inspect "^1.12.0"
|
||||
object-inspect "^1.12.2"
|
||||
pidtree "^0.5.0"
|
||||
string-argv "^0.3.1"
|
||||
supports-color "^9.2.1"
|
||||
supports-color "^9.2.2"
|
||||
yaml "^1.10.2"
|
||||
|
||||
listenercount@~1.0.1:
|
||||
@@ -16584,7 +16602,7 @@ listr2@^3.8.3:
|
||||
through "^2.3.8"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
listr2@^4.0.1:
|
||||
listr2@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5"
|
||||
integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==
|
||||
@@ -17818,7 +17836,7 @@ micromatch@^3.1.10:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.2"
|
||||
|
||||
micromatch@^4.0.2, micromatch@^4.0.4:
|
||||
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
|
||||
@@ -18357,6 +18375,17 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
ndjson@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz#320ac86f6fe53f5681897349b86ac6f43bfa3a19"
|
||||
integrity sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==
|
||||
dependencies:
|
||||
json-stringify-safe "^5.0.1"
|
||||
minimist "^1.2.5"
|
||||
readable-stream "^3.6.0"
|
||||
split2 "^3.0.0"
|
||||
through2 "^4.0.0"
|
||||
|
||||
negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
@@ -18834,11 +18863,16 @@ object-hash@^2.0.1:
|
||||
resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
|
||||
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
|
||||
|
||||
object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0:
|
||||
object-inspect@^1.11.0, object-inspect@^1.9.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
|
||||
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
|
||||
|
||||
object-inspect@^1.12.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
|
||||
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
|
||||
|
||||
object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
@@ -23551,7 +23585,7 @@ supports-color@^8.0.0, supports-color@^8.1.0, supports-color@^8.1.1:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^9.2.1:
|
||||
supports-color@^9.2.2:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz#502acaf82f2b7ee78eb7c83dcac0f89694e5a7bb"
|
||||
integrity sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA==
|
||||
|
||||
Reference in New Issue
Block a user