feat(playlists): support custom index page

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2023-10-30 17:23:29 -04:00
parent 367c819152
commit f0e2ef3b81
28 changed files with 391 additions and 187 deletions
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/plugin-playlist': minor
---
Support being able to define custom composable Playlist index pages
**BREAKING** The individual `PlaylistPage` route must now be manually hooked up by making the following change to your setup:
```diff
-import { PlaylistIndexPage } from '@backstage/plugin-playlist';
+import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
// ...
<Route path="/playlist" element={<PlaylistIndexPage />} />
+<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
```
+2 -1
View File
@@ -109,7 +109,7 @@ import { techDocsPage } from './components/techdocs/TechDocsPage';
import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { PlaylistIndexPage } from '@backstage/plugin-playlist';
import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
import { StackstormPage } from '@backstage/plugin-stackstorm';
@@ -316,6 +316,7 @@ const routes = (
<Route path="/azure-pull-requests" element={<AzurePullRequestsPage />} />
<Route path="/apache-airflow" element={<ApacheAirflowPage />} />
<Route path="/playlist" element={<PlaylistIndexPage />} />
<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
<Route path="/score-board" element={<ScoreBoardPage />} />
<Route path="/stackstorm" element={<StackstormPage />} />
<Route path="/puppetdb" element={<PuppetDbPage />} />
+16 -3
View File
@@ -23,12 +23,12 @@ yarn --cwd packages/app add @backstage/plugin-playlist
### Add the plugin to your `packages/app`
Add the root page that the playlist plugin provides to your app. You can
choose any path for the route, but we recommend the following:
Add the pages that the playlist plugin provides to your app. You can
choose any base path for the route, but we recommend the following:
```diff
// packages/app/src/App.tsx
+import { PlaylistIndexPage } from '@backstage/plugin-playlist';
+import { PlaylistIndexPage, PlaylistPage } from '@backstage/plugin-playlist';
<FlatRoutes>
@@ -37,6 +37,7 @@ choose any path for the route, but we recommend the following:
{entityPage}
</Route>
+ <Route path="/playlist" element={<PlaylistIndexPage />} />
+ <Route path="/playlist/:playlistId" element={<PlaylistPage />} />
...
</FlatRoutes>
```
@@ -140,6 +141,18 @@ playlist:
title: Collection
```
## Custom Index Page
You can customize your playlist index page by composing your own implementation. See the [`DefaultPlaylistIndexPage`](./src/components/PlaylistIndexPage/DefaultPlaylistIndexPage.tsx) for a reference of what components are available from the default setup.
```ts
- <Route path="/playlist" element={<PlaylistIndexPage />} />
+ <Route path="/playlist" element={<PlaylistIndexPage />}>
+ <CustomPlaylistIndexPage />
+ </Route>
<Route path="/playlist/:playlistId" element={<PlaylistPage />} />
```
## Features
### View All Playlists
+102
View File
@@ -13,8 +13,24 @@ import { FetchApi } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
import { Playlist } from '@backstage/plugin-playlist-common';
import { PlaylistMetadata } from '@backstage/plugin-playlist-common';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export const CreatePlaylistButton: () => React_2.JSX.Element;
// @public (undocumented)
export type DefaultPlaylistFilters = {
noop?: NoopFilter;
owners?: PlaylistOwnerFilter;
personal?: PersonalListFilter;
text?: PlaylistTextFilter;
};
// @public (undocumented)
export const DefaultPlaylistIndexPage: () => React_2.JSX.Element;
// @public (undocumented)
export const EntityPlaylistDialog: (
props: EntityPlaylistDialogProps,
@@ -35,6 +51,43 @@ export interface GetAllPlaylistsRequest {
| Record<string, string | string[] | null>;
}
// @public (undocumented)
export class NoopFilter implements PlaylistFilter {
// (undocumented)
getBackendFilters(): {
'': null;
};
}
// @public (undocumented)
export class PersonalListFilter implements PlaylistFilter {
constructor(
value: PersonalListFilterValue,
isOwnedPlaylist: (playlist: Playlist) => boolean,
);
// (undocumented)
filterPlaylist(playlist: Playlist): boolean;
// (undocumented)
readonly isOwnedPlaylist: (playlist: Playlist) => boolean;
// (undocumented)
toQueryValue(): string;
// (undocumented)
readonly value: PersonalListFilterValue;
}
// @public (undocumented)
export const enum PersonalListFilterValue {
// (undocumented)
all = 'all',
// (undocumented)
following = 'following',
// (undocumented)
owned = 'owned',
}
// @public (undocumented)
export const PersonalListPicker: () => React_2.JSX.Element;
// @public (undocumented)
export interface PlaylistApi {
// (undocumented)
@@ -93,9 +146,43 @@ export class PlaylistClient implements PlaylistApi {
updatePlaylist(playlist: PlaylistMetadata): Promise<void>;
}
// @public (undocumented)
export type PlaylistFilter = {
getBackendFilters?: () => Record<string, string | string[] | null>;
filterPlaylist?: (playlist: Playlist) => boolean;
toQueryValue?: () => string | string[];
};
// @public (undocumented)
export const PlaylistIndexPage: () => JSX_2.Element;
// @public (undocumented)
export const PlaylistList: () => React_2.JSX.Element;
// @public (undocumented)
export const PlaylistListProvider: <
PlaylistFilters extends DefaultPlaylistFilters,
>({
children,
}: PropsWithChildren<{}>) => React_2.JSX.Element;
// @public (undocumented)
export class PlaylistOwnerFilter implements PlaylistFilter {
constructor(values: string[]);
// (undocumented)
filterPlaylist(playlist: Playlist): boolean;
// (undocumented)
toQueryValue(): string[];
// (undocumented)
readonly values: string[];
}
// @public (undocumented)
export const PlaylistOwnerPicker: () => React_2.JSX.Element | null;
// @public (undocumented)
export const PlaylistPage: () => JSX_2.Element;
// @public (undocumented)
export const playlistPlugin: BackstagePlugin<
{
@@ -103,4 +190,19 @@ export const playlistPlugin: BackstagePlugin<
},
{}
>;
// @public (undocumented)
export const PlaylistSearchBar: () => React_2.JSX.Element;
// @public (undocumented)
export const PlaylistSortPicker: () => React_2.JSX.Element;
// @public (undocumented)
export class PlaylistTextFilter implements PlaylistFilter {
constructor(value: string);
// (undocumented)
filterPlaylist(playlist: Playlist): boolean;
// (undocumented)
readonly value: string;
}
```
+6 -1
View File
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { playlistPlugin, PlaylistIndexPage } from '../src/plugin';
import { PlaylistIndexPage, PlaylistPage, playlistPlugin } from '../src/plugin';
createDevApp()
.registerPlugin(playlistPlugin)
@@ -24,4 +24,9 @@ createDevApp()
title: 'Root Page',
path: '/playlist',
})
.addPage({
element: <PlaylistPage />,
title: 'Root Page',
path: '/playlist/:playlistId',
})
.render();
@@ -26,7 +26,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import React from 'react';
import { SWRConfig } from 'swr';
import { PlaylistApi, playlistApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { playlistRouteRef, rootRouteRef } from '../../routes';
import { CreatePlaylistButton } from './CreatePlaylistButton';
jest.mock('../PlaylistEditDialog', () => ({
@@ -73,7 +73,12 @@ describe('<CreatePlaylistButton/>', () => {
<CreatePlaylistButton />
</TestApiProvider>
</SWRConfig>,
{ mountedRoutes: { '/playlists': rootRouteRef } },
{
mountedRoutes: {
'/playlists': rootRouteRef,
'/playlists/:playlistId': playlistRouteRef,
},
},
);
};
@@ -36,6 +36,9 @@ import { playlistRouteRef } from '../../routes';
import { PlaylistEditDialog } from '../PlaylistEditDialog';
import { useTitle } from '../../hooks';
/**
* @public
*/
export const CreatePlaylistButton = () => {
const navigate = useNavigate();
const errorApi = useApi(errorApiRef);
@@ -28,7 +28,7 @@ import { fireEvent, getByRole, waitFor, act } from '@testing-library/react';
import React from 'react';
import { SWRConfig } from 'swr';
import { PlaylistApi, playlistApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { playlistRouteRef, rootRouteRef } from '../../routes';
import { EntityPlaylistDialog } from './EntityPlaylistDialog';
const navigateMock = jest.fn();
@@ -103,7 +103,12 @@ describe('EntityPlaylistDialog', () => {
</TestApiProvider>
,
</SWRConfig>,
{ mountedRoutes: { '/playlists': rootRouteRef } },
{
mountedRoutes: {
'/playlists': rootRouteRef,
'/playlists/:playlistId': playlistRouteRef,
},
},
);
beforeEach(() => {
@@ -41,12 +41,18 @@ import useAsync from 'react-use/lib/useAsync';
import { usePlaylistList } from '../../hooks';
import { PlaylistFilter } from '../../types';
/**
* @public
*/
export const enum PersonalListFilterValue {
owned = 'owned',
following = 'following',
all = 'all',
}
/**
* @public
*/
export class PersonalListFilter implements PlaylistFilter {
constructor(
readonly value: PersonalListFilterValue,
@@ -131,6 +137,9 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] {
];
}
/**
* @public
*/
export const PersonalListPicker = () => {
const classes = useStyles();
const configApi = useApi(configApiRef);
@@ -18,7 +18,7 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { rootRouteRef } from '../../routes';
import { playlistRouteRef, rootRouteRef } from '../../routes';
import { PlaylistCard } from './PlaylistCard';
describe('<PlaylistCard/>', () => {
@@ -39,6 +39,7 @@ describe('<PlaylistCard/>', () => {
{
mountedRoutes: {
'/playlists': rootRouteRef,
'/playlists/:playlistId': playlistRouteRef,
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
@@ -0,0 +1,57 @@
/*
* Copyright 2023 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import React from 'react';
import { PlaylistApi, playlistApiRef } from '../../api';
import { playlistRouteRef, rootRouteRef } from '../../routes';
import { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
const playlistApi: Partial<PlaylistApi> = {
getAllPlaylists: async () => [],
};
const permissionApi: Partial<PermissionApi> = {
authorize: async () => ({ result: AuthorizeResult.ALLOW }),
};
describe('DefaultPlaylistIndexPage', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[permissionApiRef, permissionApi],
[playlistApiRef, playlistApi],
]}
>
<DefaultPlaylistIndexPage />
</TestApiProvider>,
{
mountedRoutes: {
'/playlists': rootRouteRef,
'/playlists/:playlistId': playlistRouteRef,
},
},
);
expect(rendered.getByText('Playlists')).toBeInTheDocument();
});
});
@@ -0,0 +1,67 @@
/*
* Copyright 2023 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 from 'react';
import {
PageWithHeader,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
import { CreatePlaylistButton } from '../CreatePlaylistButton';
import { PersonalListPicker } from '../PersonalListPicker';
import { PlaylistList } from '../PlaylistList';
import { PlaylistOwnerPicker } from '../PlaylistOwnerPicker';
import { PlaylistSearchBar } from '../PlaylistSearchBar';
import { PlaylistSortPicker } from '../PlaylistSortPicker';
import { PlaylistListProvider } from '../../hooks/PlaylistListProvider';
import { useTitle } from '../../hooks/useTitle';
/**
* @public
*/
export const DefaultPlaylistIndexPage = () => {
const pluralTitle = useTitle({
pluralize: true,
lowerCase: false,
});
return (
<PageWithHeader themeId="home" title={pluralTitle}>
<PlaylistListProvider>
<Content>
<ContentHeader title="">
<PlaylistSortPicker />
<CreatePlaylistButton />
<SupportButton />
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<PlaylistSearchBar />
<PersonalListPicker />
<PlaylistOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<PlaylistList />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</PlaylistListProvider>
</PageWithHeader>
);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2022 The Backstage Authors
* Copyright 2023 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.
@@ -14,39 +14,33 @@
* limitations under the License.
*/
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import React from 'react';
import { PlaylistApi, playlistApiRef } from '../../api';
import { rootRouteRef } from '../../routes';
import { renderInTestApp } from '@backstage/test-utils';
import { useOutlet } from 'react-router-dom';
import { PlaylistIndexPage } from './PlaylistIndexPage';
const playlistApi: Partial<PlaylistApi> = {
getAllPlaylists: async () => [],
};
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useOutlet: jest.fn().mockReturnValue('Route Children'),
}));
const permissionApi: Partial<PermissionApi> = {
authorize: async () => ({ result: AuthorizeResult.ALLOW }),
};
jest.mock('./DefaultPlaylistIndexPage', () => ({
DefaultPlaylistIndexPage: jest
.fn()
.mockReturnValue('DefaultPlaylistIndexPage'),
}));
describe('PlaylistIndexPage', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[permissionApiRef, permissionApi],
[playlistApiRef, playlistApi],
]}
>
<PlaylistIndexPage />
</TestApiProvider>,
{ mountedRoutes: { '/playlists': rootRouteRef } },
);
expect(rendered.getByText('Playlists')).toBeInTheDocument();
it('renders provided router element', async () => {
const { getByText } = await renderInTestApp(<PlaylistIndexPage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
it('renders DefaultPlaylistIndexPage when no router children are provided', async () => {
(useOutlet as jest.Mock).mockReturnValueOnce(null);
const { getByText } = await renderInTestApp(<PlaylistIndexPage />);
expect(getByText('DefaultPlaylistIndexPage')).toBeInTheDocument();
});
});
@@ -1,5 +1,5 @@
/*
* Copyright 2022 The Backstage Authors
* Copyright 2023 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.
@@ -15,50 +15,11 @@
*/
import React from 'react';
import {
PageWithHeader,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core-components';
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
import { CreatePlaylistButton } from '../CreatePlaylistButton';
import { PersonalListPicker } from '../PersonalListPicker';
import { PlaylistList } from '../PlaylistList';
import { PlaylistOwnerPicker } from '../PlaylistOwnerPicker';
import { PlaylistSearchBar } from '../PlaylistSearchBar';
import { PlaylistSortPicker } from '../PlaylistSortPicker';
import { PlaylistListProvider } from '../../hooks/PlaylistListProvider';
import { useTitle } from '../../hooks/useTitle';
import { useOutlet } from 'react-router-dom';
import { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
export const PlaylistIndexPage = () => {
const pluralTitle = useTitle({
pluralize: true,
lowerCase: false,
});
const outlet = useOutlet();
return (
<PageWithHeader themeId="home" title={pluralTitle}>
<PlaylistListProvider>
<Content>
<ContentHeader title="">
<PlaylistSortPicker />
<CreatePlaylistButton />
<SupportButton />
</ContentHeader>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<PlaylistSearchBar />
<PersonalListPicker />
<PlaylistOwnerPicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<PlaylistList />
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</Content>
</PlaylistListProvider>
</PageWithHeader>
);
return outlet || <DefaultPlaylistIndexPage />;
};
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DefaultPlaylistIndexPage } from './DefaultPlaylistIndexPage';
export { PlaylistIndexPage } from './PlaylistIndexPage';
@@ -26,6 +26,9 @@ import { Typography } from '@material-ui/core';
import { useTitle, usePlaylistList } from '../../hooks';
import { PlaylistCard } from '../PlaylistCard';
/**
* @public
*/
export const PlaylistList = () => {
const { loading, error, playlists } = usePlaylistList();
const pluralTitleLowerCase = useTitle({
@@ -34,6 +34,9 @@ import React, { useEffect, useMemo, useState } from 'react';
import { usePlaylistList } from '../../hooks';
import { PlaylistFilter } from '../../types';
/**
* @public
*/
export class PlaylistOwnerFilter implements PlaylistFilter {
constructor(readonly values: string[]) {}
@@ -49,6 +52,9 @@ export class PlaylistOwnerFilter implements PlaylistFilter {
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
/**
* @public
*/
export const PlaylistOwnerPicker = () => {
const {
updateFilters,
@@ -31,6 +31,9 @@ import useDebounce from 'react-use/lib/useDebounce';
import { usePlaylistList } from '../../hooks';
import { PlaylistFilter } from '../../types';
/**
* @public
*/
export class PlaylistTextFilter implements PlaylistFilter {
constructor(readonly value: string) {}
@@ -52,6 +55,9 @@ const useStyles = makeStyles(_theme => ({
},
}));
/**
* @public
*/
export const PlaylistSearchBar = () => {
const classes = useStyles();
@@ -74,6 +74,9 @@ const useStyles = makeStyles({
},
});
/**
* @public
*/
export const PlaylistSortPicker = () => {
const classes = useStyles();
const { updateSort } = usePlaylistList();
@@ -1,51 +0,0 @@
/*
* 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 from 'react';
import { PlaylistIndexPage } from '../PlaylistIndexPage';
import { PlaylistPage } from '../PlaylistPage';
import { Router } from './Router';
import { renderInTestApp } from '@backstage/test-utils';
jest.mock('../PlaylistIndexPage', () => ({
PlaylistIndexPage: jest.fn(() => null),
}));
jest.mock('../PlaylistPage', () => ({
PlaylistPage: jest.fn(() => null),
}));
describe('Router', () => {
beforeEach(() => {
(PlaylistPage as jest.Mock).mockClear();
(PlaylistIndexPage as jest.Mock).mockClear();
});
describe('/', () => {
it('should render the PlaylistIndexPage', async () => {
await renderInTestApp(<Router />);
expect(PlaylistIndexPage).toHaveBeenCalled();
});
});
describe('/:playlistId', () => {
it('should render the PlaylistPage page', async () => {
await renderInTestApp(<Router />, {
routeEntries: ['/my-playlist'],
});
expect(PlaylistPage).toHaveBeenCalled();
});
});
});
@@ -1,29 +0,0 @@
/*
* 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 from 'react';
import { Routes, Route } from 'react-router-dom';
import { playlistRouteRef } from '../../routes';
import { PlaylistIndexPage } from '../PlaylistIndexPage';
import { PlaylistPage } from '../PlaylistPage';
export const Router = () => (
<Routes>
<Route path="/" element={<PlaylistIndexPage />} />
<Route path={playlistRouteRef.path} element={<PlaylistPage />} />
</Routes>
);
@@ -1,17 +0,0 @@
/*
* 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.
*/
export * from './Router';
@@ -58,6 +58,9 @@ type OutputState<PlaylistFilters extends DefaultPlaylistFilters> = {
backendPlaylists: Playlist[];
};
/**
* @public
*/
export const PlaylistListProvider = <
PlaylistFilters extends DefaultPlaylistFilters,
>({
@@ -21,12 +21,18 @@ import { PlaylistOwnerFilter } from '../components/PlaylistOwnerPicker';
import { PlaylistTextFilter } from '../components/PlaylistSearchBar';
import { PlaylistFilter, PlaylistSortCompareFunction } from '../types';
/**
* @public
*/
export class NoopFilter implements PlaylistFilter {
getBackendFilters() {
return { '': null };
}
}
/**
* @public
*/
export type DefaultPlaylistFilters = {
noop?: NoopFilter;
owners?: PlaylistOwnerFilter;
+19 -1
View File
@@ -19,10 +19,28 @@
*
* @packageDocumentation
*/
export * from './api';
export {
CreatePlaylistButton,
DefaultPlaylistIndexPage,
PersonalListFilter,
PersonalListFilterValue,
PersonalListPicker,
PlaylistList,
PlaylistOwnerFilter,
PlaylistOwnerPicker,
PlaylistSearchBar,
PlaylistSortPicker,
PlaylistTextFilter,
} from './components';
export type { EntityPlaylistDialogProps } from './components';
export { NoopFilter } from './hooks';
export type { DefaultPlaylistFilters } from './hooks';
export { PlaylistListProvider } from './hooks/PlaylistListProvider';
export {
EntityPlaylistDialog,
PlaylistPage,
playlistPlugin,
PlaylistIndexPage,
} from './plugin';
export * from './api';
export type { PlaylistFilter } from './types';
+15 -2
View File
@@ -24,7 +24,7 @@ import {
} from '@backstage/core-plugin-api';
import { playlistApiRef, PlaylistClient } from './api';
import { rootRouteRef } from './routes';
import { playlistRouteRef, rootRouteRef } from './routes';
/**
* @public
@@ -53,11 +53,24 @@ export const playlistPlugin = createPlugin({
export const PlaylistIndexPage = playlistPlugin.provide(
createRoutableExtension({
name: 'PlaylistIndexPage',
component: () => import('./components/Router').then(m => m.Router),
component: () =>
import('./components/PlaylistIndexPage').then(m => m.PlaylistIndexPage),
mountPoint: rootRouteRef,
}),
);
/**
* @public
*/
export const PlaylistPage = playlistPlugin.provide(
createRoutableExtension({
name: 'PlaylistPage',
component: () =>
import('./components/PlaylistPage').then(m => m.PlaylistPage),
mountPoint: playlistRouteRef,
}),
);
/**
* @public
*/
+3 -4
View File
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'playlist:index-page',
});
export const playlistRouteRef = createSubRouteRef({
export const playlistRouteRef = createRouteRef({
id: 'playlist:playlist-page',
parent: rootRouteRef,
path: '/:playlistId',
params: ['playlistId'],
});
+3
View File
@@ -16,6 +16,9 @@
import { Playlist } from '@backstage/plugin-playlist-common';
/**
* @public
*/
export type PlaylistFilter = {
getBackendFilters?: () => Record<string, string | string[] | null>;